Testing

Testing

Whenever you write a new line of code, you also potentially add new bugs. To build better and more reliable applications, you should test your code using both functional and unit tests.

The PHPUnit Testing Framework

Symfony2 integrates with an independent library - called PHPUnit - to give you a rich testing framework. This chapter won't cover PHPUnit itself, but it has its own excellent documentation.

Note

Symfony2 works with PHPUnit 3.5.11 or later, though version 3.6.4 is needed to test the Symfony core code itself.

Each test - whether it's a unit test or a functional test - is a PHP class that should live in the Tests/ subdirectory of your bundles. If you follow this rule, then you can run all of your application's tests with the following command:

1
2
# specify the configuration directory on the command line
$ phpunit -c app/

The -c option tells PHPUnit to look in the app/ directory for a configuration file. If you're curious about the PHPUnit options, check out the app/phpunit.xml.dist file.

Tip

Code coverage can be generated with the --coverage-html option.

Unit Tests

A unit test is usually a test against a specific PHP class. If you want to test the overall behavior of your application, see the section about Functional Tests.

Writing Symfony2 unit tests is no different than writing standard PHPUnit unit tests. Suppose, for example, that you have an incredibly simple class called Calculator in the Utility/ directory of your bundle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
// src/Acme/DemoBundle/Utility/Calculator.php
namespace Acme\DemoBundle\Utility;

class Calculator
{
    public function add($a, $b)
    {
        return $a + $b;
    }
}

To test this, create a CalculatorTest file in the Tests/Utility directory of your bundle:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php
namespace Acme\DemoBundle\Tests\Utility;

use Acme\DemoBundle\Utility\Calculator;

class CalculatorTest extends \PHPUnit_Framework_TestCase
{
    public function testAdd()
    {
        $calc = new Calculator();
        $result = $calc->add(30, 12);

        // assert that your calculator added the numbers correctly!
        $this->assertEquals(42, $result);
    }
}

Note

By convention, the Tests/ sub-directory should replicate the directory of your bundle. So, if you're testing a class in your bundle's Utility/ directory, put the test in the Tests/Utility/ directory.

Just like in your real application - autoloading is automatically enabled via the bootstrap.php.cache file (as configured by default in the app/phpunit.xml.dist file).

Running tests for a given file or directory is also very easy:

1
2
3
4
5
6
7
8
# run all tests in the Utility directory
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/

# run tests for the Calculator class
$ phpunit -c app src/Acme/DemoBundle/Tests/Utility/CalculatorTest.php

# run all tests for the entire Bundle
$ phpunit -c app src/Acme/DemoBundle/

Functional Tests

Functional tests check the integration of the different layers of an application (from the routing to the views). They are no different from unit tests as far as PHPUnit is concerned, but they have a very specific workflow:

  • Make a request;
  • Test the response;
  • Click on a link or submit a form;
  • Test the response;
  • Rinse and repeat.

Your First Functional Test

Functional tests are simple PHP files that typically live in the Tests/Controller directory of your bundle. If you want to test the pages handled by your DemoController class, start by creating a new DemoControllerTest.php file that extends a special WebTestCase class.

For example, the Symfony2 Standard Edition provides a simple functional test for its DemoController (DemoControllerTest) that reads as follows:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
// src/Acme/DemoBundle/Tests/Controller/DemoControllerTest.php
namespace Acme\DemoBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class DemoControllerTest extends WebTestCase
{
    public function testIndex()
    {
        $client = static::createClient();

        $crawler = $client->request('GET', '/demo/hello/Fabien');

        $this->assertGreaterThan(
            0,
            $crawler->filter('html:contains("Hello Fabien")')->count()
        );
    }
}

Tip

To run your functional tests, the WebTestCase class bootstraps the kernel of your application. In most cases, this happens automatically. However, if your kernel is in a non-standard directory, you'll need to modify your phpunit.xml.dist file to set the KERNEL_DIR environment variable to the directory of your kernel:

1
2
3
4
5
6
7
<phpunit>
    <!-- ... -->
    <php>
        <server name="KERNEL_DIR" value="/path/to/your/app/" />
    </php>
    <!-- ... -->
</phpunit>

The createClient() method returns a client, which is like a browser that you'll use to crawl your site:

1
$crawler = $client->request('GET', '/demo/hello/Fabien');

The request() method (see more about the request method) returns a Crawler object which can be used to select elements in the Response, click on links, and submit forms.

Tip

The Crawler only works when the response is an XML or an HTML document. To get the raw content response, call $client->getResponse()->getContent().

Click on a link by first selecting it with the Crawler using either an XPath expression or a CSS selector, then use the Client to click on it. For example, the following code finds all links with the text Greet, then selects the second one, and ultimately clicks on it:

1
2
3
$link = $crawler->filter('a:contains("Greet")')->eq(1)->link();

$crawler = $client->click($link);

Submitting a form is very similar; select a form button, optionally override some form values, and submit the corresponding form:

1
2
3
4
5
6
7
8
$form = $crawler->selectButton('submit')->form();

// set some values
$form['name'] = 'Lucas';
$form['form_name[subject]'] = 'Hey there!';

// submit the form
$crawler = $client->submit($form);

Tip

The form can also handle uploads and contains methods to fill in different types of form fields (e.g. select() and tick()). For details, see the Forms section below.

Now that you can easily navigate through an application, use assertions to test that it actually does what you expect it to. Use the Crawler to make assertions on the DOM:

1
2
// Assert that the response matches a given CSS selector.
$this->assertGreaterThan(0, $crawler->filter('h1')->count());

Or, test against the Response content directly if you just want to assert that the content contains some text, or if the Response is not an XML/HTML document:

1
2
3
4
$this->assertRegExp(
    '/Hello Fabien/',
    $client->getResponse()->getContent()
);

The full signature of the request() method is:

1
2
3
4
5
6
7
8
9
request(
    $method,
    $uri,
    array $parameters = array(),
    array $files = array(),
    array $server = array(),
    $content = null,
    $changeHistory = true
)

The server array is the raw values that you'd expect to normally find in the PHP $_SERVER superglobal. For example, to set the Content-Type, Referer and X-Requested-With HTTP headers, you'd pass the following (mind the HTTP_ prefix for non standard headers):

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
$client->request(
    'GET',
    '/demo/hello/Fabien',
    array(),
    array(),
    array(
        'CONTENT_TYPE'          => 'application/json',
        'HTTP_REFERER'          => '/foo/bar',
        'HTTP_X-Requested-With' => 'XMLHttpRequest',
    )
);

To get you started faster, here is a list of the most common and useful test assertions:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
use Symfony\Component\HttpFoundation\Response;

// ...

// Assert that there is at least one h2 tag
// with the class "subtitle"
$this->assertGreaterThan(
    0,
    $crawler->filter('h2.subtitle')->count()
);

// Assert that there are exactly 4 h2 tags on the page
$this->assertCount(4, $crawler->filter('h2'));

// Assert that the "Content-Type" header is "application/json"
$this->assertTrue(
    $client->getResponse()->headers->contains(
        'Content-Type',
        'application/json'
    )
);

// Assert that the response content matches a regexp.
$this->assertRegExp('/foo/', $client->getResponse()->getContent());

// Assert that the response status code is 2xx
$this->assertTrue($client->getResponse()->isSuccessful());
// Assert that the response status code is 404
$this->assertTrue($client->getResponse()->isNotFound());
// Assert a specific 200 status code
$this->assertEquals(
    Response::HTTP_OK,
    $client->getResponse()->getStatusCode()
);

// Assert that the response is a redirect to /demo/contact
$this->assertTrue(
    $client->getResponse()->isRedirect('/demo/contact')
);
// or simply check that the response is a redirect to any URL
$this->assertTrue($client->getResponse()->isRedirect());

2.4Support for HTTP status code constants was introduced in Symfony 2.4.

Working with the Test Client

The Test Client simulates an HTTP client like a browser and makes requests into your Symfony2 application:

1
$crawler = $client->request('GET', '/hello/Fabien');

The request() method takes the HTTP method and a URL as arguments and returns a Crawler instance.

Tip

Hardcoding the request URLs is a best practice for functional tests. If the test generates URLs using the Symfony router, it won't detect any change made to the application URLs which may impact the end users.

Use the Crawler to find DOM elements in the Response. These elements can then be used to click on links and submit forms:

1
2
3
4
5
$link = $crawler->selectLink('Go elsewhere...')->link();
$crawler = $client->click($link);

$form = $crawler->selectButton('validate')->form();
$crawler = $client->submit($form, array('name' => 'Fabien'));

The click() and submit() methods both return a Crawler object. These methods are the best way to browse your application as it takes care of a lot of things for you, like detecting the HTTP method from a form and giving you a nice API for uploading files.

Tip

You will learn more about the Link and Form objects in the Crawler section below.

The request method can also be used to simulate form submissions directly or perform more complex requests:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// Directly submit a form (but using the Crawler is easier!)
$client->request('POST', '/submit', array('name' => 'Fabien'));

// Submit a raw JSON string in the request body
$client->request(
    'POST',
    '/submit',
    array(),
    array(),
    array('CONTENT_TYPE' => 'application/json'),
    '{"name":"Fabien"}'
);

// Form submission with a file upload
use Symfony\Component\HttpFoundation\File\UploadedFile;

$photo = new UploadedFile(
    '/path/to/photo.jpg',
    'photo.jpg',
    'image/jpeg',
    123
);
$client->request(
    'POST',
    '/submit',
    array('name' => 'Fabien'),
    array('photo' => $photo)
);

// Perform a DELETE requests, and pass HTTP headers
$client->request(
    'DELETE',
    '/post/12',
    array(),
    array(),
    array('PHP_AUTH_USER' => 'username', 'PHP_AUTH_PW' => 'pa$$word')
);

Last but not least, you can force each request to be executed in its own PHP process to avoid any side-effects when working with several clients in the same script:

1
$client->insulate();

Browsing

The Client supports many operations that can be done in a real browser:

1
2
3
4
5
6
$client->back();
$client->forward();
$client->reload();

// Clears all cookies and the history
$client->restart();

Accessing internal Objects

2.3The getInternalRequest() and getInternalResponse() methods were introduced in Symfony 2.3.

If you use the client to test your application, you might want to access the client's internal objects:

1
2
$history   = $client->getHistory();
$cookieJar = $client->getCookieJar();

You can also get the objects related to the latest request:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
// the HttpKernel request instance
$request  = $client->getRequest();

// the BrowserKit request instance
$request  = $client->getInternalRequest();

// the HttpKernel response instance
$response = $client->getResponse();

// the BrowserKit response instance
$response = $client->getInternalResponse();

$crawler  = $client->getCrawler();

If your requests are not insulated, you can also access the Container and the Kernel:

1
2
$container = $client->getContainer();
$kernel    = $client->getKernel();

Accessing the Container

It's highly recommended that a functional test only tests the Response. But under certain very rare circumstances, you might want to access some internal objects to write assertions. In such cases, you can access the dependency injection container:

1
$container = $client->getContainer();

Be warned that this does not work if you insulate the client or if you use an HTTP layer. For a list of services available in your application, use the container:debug console task.

Tip

If the information you need to check is available from the profiler, use it instead.

Accessing the Profiler Data

On each request, you can enable the Symfony profiler to collect data about the internal handling of that request. For example, the profiler could be used to verify that a given page executes less than a certain number of database queries when loading.

To get the Profiler for the last request, do the following:

1
2
3
4
5
6
7
// enable the profiler for the very next request
$client->enableProfiler();

$crawler = $client->request('GET', '/profiler');

// get the profile
$profile = $client->getProfile();

For specific details on using the profiler inside a test, see the How to use the Profiler in a Functional Test cookbook entry.

Redirecting

When a request returns a redirect response, the client does not follow it automatically. You can examine the response and force a redirection afterwards with the followRedirect() method:

1
$crawler = $client->followRedirect();

If you want the client to automatically follow all redirects, you can force him with the followRedirects() method:

1
$client->followRedirects();

If you pass false to the followRedirects() method, the redirects will no longer be followed:

1
$client->followRedirects(false);

The Crawler

A Crawler instance is returned each time you make a request with the Client. It allows you to traverse HTML documents, select nodes, find links and forms.

Traversing

Like jQuery, the Crawler has methods to traverse the DOM of an HTML/XML document. For example, the following finds all input[type=submit] elements, selects the last one on the page, and then selects its immediate parent element:

1
2
3
4
5
$newCrawler = $crawler->filter('input[type=submit]')
    ->last()
    ->parents()
    ->first()
;

Many other methods are also available:

Method Description
filter('h1.title') Nodes that match the CSS selector
filterXpath('h1') Nodes that match the XPath expression
eq(1) Node for the specified index
first() First node
last() Last node
siblings() Siblings
nextAll() All following siblings
previousAll() All preceding siblings
parents() Returns the parent nodes
children() Returns children nodes
reduce($lambda) Nodes for which the callable does not return false

Since each of these methods returns a new Crawler instance, you can narrow down your node selection by chaining the method calls:

1
2
3
4
5
6
7
8
$crawler
    ->filter('h1')
    ->reduce(function ($node, $i) {
        if (!$node->getAttribute('class')) {
            return false;
        }
    })
    ->first();

Tip

Use the count() function to get the number of nodes stored in a Crawler: count($crawler)

Extracting Information

The Crawler can extract information from the nodes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
// Returns the attribute value for the first node
$crawler->attr('class');

// Returns the node value for the first node
$crawler->text();

// Extracts an array of attributes for all nodes
// (_text returns the node value)
// returns an array for each element in crawler,
// each with the value and href
$info = $crawler->extract(array('_text', 'href'));

// Executes a lambda for each node and return an array of results
$data = $crawler->each(function ($node, $i) {
    return $node->attr('href');
});

Forms

Just like links, you select forms with the selectButton() method:

1
$buttonCrawlerNode = $crawler->selectButton('submit');

Note

Notice that you select form buttons and not forms as a form can have several buttons; if you use the traversing API, keep in mind that you must look for a button.

The selectButton() method can select button tags and submit input tags. It uses several different parts of the buttons to find them:

  • The value attribute value;
  • The id or alt attribute value for images;
  • The id or name attribute value for button tags.

Once you have a Crawler representing a button, call the form() method to get a Form instance for the form wrapping the button node:

1
$form = $buttonCrawlerNode->form();

When calling the form() method, you can also pass an array of field values that overrides the default ones:

1
2
3
4
$form = $buttonCrawlerNode->form(array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

And if you want to simulate a specific HTTP method for the form, pass it as a second argument:

1
$form = $buttonCrawlerNode->form(array(), 'DELETE');

The Client can submit Form instances:

1
$client->submit($form);

The field values can also be passed as a second argument of the submit() method:

1
2
3
4
$client->submit($form, array(
    'name'              => 'Fabien',
    'my_form[subject]'  => 'Symfony rocks!',
));

For more complex situations, use the Form instance as an array to set the value of each field individually:

1
2
3
// Change the value of a field
$form['name'] = 'Fabien';
$form['my_form[subject]'] = 'Symfony rocks!';

There is also a nice API to manipulate the values of the fields according to their type:

1
2
3
4
5
6
7
8
// Select an option or a radio
$form['country']->select('France');

// Tick a checkbox
$form['like_symfony']->tick();

// Upload a file
$form['photo']->upload('/path/to/lucas.jpg');

Tip

If you purposefully want to select "invalid" select/radio values, see Selecting Invalid Choice Values.

Tip

You can get the values that will be submitted by calling the getValues() method on the Form object. The uploaded files are available in a separate array returned by getFiles(). The getPhpValues() and getPhpFiles() methods also return the submitted values, but in the PHP format (it converts the keys with square brackets notation - e.g. my_form[subject] - to PHP arrays).

Testing Configuration

The Client used by functional tests creates a Kernel that runs in a special test environment. Since Symfony loads the app/config/config_test.yml in the test environment, you can tweak any of your application's settings specifically for testing.

For example, by default, the Swift Mailer is configured to not actually deliver emails in the test environment. You can see this under the swiftmailer configuration option:

  • YAML
    1
    2
    3
    4
    5
    # app/config/config_test.yml
    
    # ...
    swiftmailer:
        disable_delivery: true
    
  • XML
     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    <!-- app/config/config_test.xml -->
    <?xml version="1.0" encoding="UTF-8" ?>
    <container xmlns="http://symfony.com/schema/dic/services"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:swiftmailer="http://symfony.com/schema/dic/swiftmailer"
        xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd
                            http://symfony.com/schema/dic/swiftmailer http://symfony.com/schema/dic/swiftmailer/swiftmailer-1.0.xsd">
    
        <!-- ... -->
        <swiftmailer:config disable-delivery="true" />
    </container>
    
  • PHP
    1
    2
    3
    4
    5
    6
    // app/config/config_test.php
    
    // ...
    $container->loadFromExtension('swiftmailer', array(
        'disable_delivery' => true,
    ));
    

You can also use a different environment entirely, or override the default debug mode (true) by passing each as options to the createClient() method:

1
2
3
4
$client = static::createClient(array(
    'environment' => 'my_test_env',
    'debug'       => false,
));

If your application behaves according to some HTTP headers, pass them as the second argument of createClient():

1
2
3
4
$client = static::createClient(array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

You can also override HTTP headers on a per request basis:

1
2
3
4
$client->request('GET', '/', array(), array(), array(
    'HTTP_HOST'       => 'en.example.com',
    'HTTP_USER_AGENT' => 'MySuperBrowser/1.0',
));

Tip

The test client is available as a service in the container in the test environment (or wherever the framework.test option is enabled). This means you can override the service entirely if you need to.

PHPUnit Configuration

Each application has its own PHPUnit configuration, stored in the app/phpunit.xml.dist file. You can edit this file to change the defaults or create an app/phpunit.xml file to setup a configuration for your local machine only.

Tip

Store the app/phpunit.xml.dist file in your code repository and ignore the app/phpunit.xml file.

By default, only the tests from your own custom bundles stored in the standard directories src/*/*Bundle/Tests or src/*/Bundle/*Bundle/Tests are run by the phpunit command, as configured in the app/phpunit.xml.dist file:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <directory>../src/*/*Bundle/Tests</directory>
            <directory>../src/*/Bundle/*Bundle/Tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... -->
</phpunit>

But you can easily add more directories. For instance, the following configuration adds tests from a custom lib/tests directory:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <testsuites>
        <testsuite name="Project Test Suite">
            <!-- ... --->
            <directory>../lib/tests</directory>
        </testsuite>
    </testsuites>
    <!-- ... --->
</phpunit>

To include other directories in the code coverage, also edit the <filter> section:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
<!-- app/phpunit.xml.dist -->
<phpunit>
    <!-- ... -->
    <filter>
        <whitelist>
            <!-- ... -->
            <directory>../lib</directory>
            <exclude>
                <!-- ... -->
                <directory>../lib/tests</directory>
            </exclude>
        </whitelist>
    </filter>
    <!-- ... --->
</phpunit>

This work is licensed under a Creative Commons Attribution-Share Alike 3.0 Unported License .