This tutorial has a new version, check it out!

Functional Testing

Keep on Learning!

If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.

Start your All-Access Pass
Buy just this tutorial for $12.00

Functional Testing

Our site is looking cool. But how can we be sure that we haven’t broken anything along the way? Right now, we can’t!

Let’s avoid the future angry phone calls from clients by adding some tests. There are two main types: unit tests and functional tests. Unit tests test individual PHP classes. We’ll save that topic for another screencast. Functional tests are more like a browser that surfs to pages on your site, fills out forms and checks for specific things.

Your First Functional Test

When we generated the EventBundle in the last screencast, it created 2 stub functional tests for us. How nice!

Create a Tests/Controller directory in UserBundle, copy one of the test files and rename it to RegisterControllerTest:

// src/Yoda/UserBundle/Tests/Controller/RegisterControllerTest.php
namespace Yoda\UserBundle\Tests\Controller;

use Symfony\Bundle\FrameworkBundle\Test\WebTestCase;

class RegisterControllerTest extends WebTestCase
{
    public function testRegister()
    {
        $client = static::createClient();
        // ...
    }
}

Rename the method to testRegister:

// src/Yoda/UserBundle/Tests/Controller/RegisterControllerTest.php
// ...

public function testRegister()
{
    $client = static::createClient();
    // ...
}

The idea is that each controller, like RegisterController will have its own test class, like RegisterControllerTest. Then, each action method, like registerAction, will have its own test method, like testRegister. There’s no technical reason you need to organize things like this. The only rule is that you need to start each method with the word “test”.

Using the Client object

That $client variable is like a browser that we can use to surf to pages on our site. Start small by testing that the /register page returns a 200 status code and that the word “Register” appears somewhere:

public function testRegister()
{
    $client = static::createClient();

    $client->request('GET', '/register');
    $response = $client->getResponse();

    $this->assertEquals(200, $response->getStatusCode());
    $this->assertContains('Register', $response->getContent());
}

The assertEquals and assertContains methods come from PHPUnit, the library that will actually run the test.

Installing PHPUnit

To run the test, we need PHPUnit: the de-facto tool for testing. You can install it globally or locally in this project via Composer. For the global option, check out their docs.

Let’s use Composer’s require command and search for phpunit:

php composer.phar require

Choose the phpunit/phpunit result. For a version, I’ll go to packagist.org and find the library. Right now, it looks like the latest version is 4.1.3. I’ll use the constraint ~4.1, which basically means 4.1 or higher.

Tip

Want to know more about the ~ version constraint? Read Next Significant Release on Composer’s website.

This added phpunit/phpunit to the require key in composer.json and it ran the update command in the background to download it.

Tip

Since PHPUnit isn’t actually needed to make our site work (it’s only needed to run the tests), it would be even better to put it in the require-dev key of composer.json. Search for require-dev on this post for more details.

Running the Tests

We now have a bin/phpunit executable, so let’s use it! Pass it a -c app option:

php bin/phpunit -c app

Tip

If you’re on Windows (or a VM running in Windows), the above command won’t work for you (it’ll just spit out some text). Instead, run:

bin\phpunit -c app

This tells PHPUnit to look for a configuration file in the app/ directory. And hey! There’s a phpunit.xml.dist file there already for it to read. This tells phpunit how to bootstrap and where to find our tests.

But we see a few errors. If you look closely, you’ll see that it’s executing the two test files that were generated automatically in EventBundle. Git rid of these troublemakers and try again:

rm src/Yoda/EventBundle/Tests/Controller/*Test.php
php bin/phpunit -c app

Green! PHPUnit runs our test, where we make a request to /register and check the status code and look for the word “Register”.

To see what a failed test looks like, change the test to check for Ackbar instead of Resgister and re-run it:

$this->assertContains('Ackbar', $response->getContent());

It doesn’t find it, but it does print out the page’s content, which we could use to debug. It’s a trap! Change the test back to look for Register:

$this->assertContains('Register', $response->getContent());

Traversing the Dom with the Crawler

When we call the request() function, it returns a Crawler object, which works a lot like the jQuery object in JavaScript. For example, to find the value of the username field, we can search by its id and use the attr function. It should be equal to “Leia”:

public function testRegister()
{
    $client = static::createClient();

    $crawler = $client->request('GET', '/register');
    $response = $client->getResponse();

    $this->assertEquals(200, $response->getStatusCode());
    $this->assertContains('Register', $response->getContent());

    $usernameVal = $crawler
        ->filter('#user_register_username')
        ->attr('value')
    ;
    $this->assertEquals('Leia', $usernameVal);
}

Re-run the test to see the result:

php bin/phpunit -c app

Tip

To see everything about the crawler, check out The DomCrawler Component.

Leave a comment!

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "php": ">=5.3.3",
        "symfony/symfony": "~2.4", // v2.4.2
        "doctrine/orm": "~2.2,>=2.2.3", // v2.4.2
        "doctrine/doctrine-bundle": "~1.2", // v1.2.0
        "twig/extensions": "~1.0", // v1.0.1
        "symfony/assetic-bundle": "~2.3", // v2.3.0
        "symfony/swiftmailer-bundle": "~2.3", // v2.3.5
        "symfony/monolog-bundle": "~2.4", // v2.5.0
        "sensio/distribution-bundle": "~2.3", // v2.3.4
        "sensio/framework-extra-bundle": "~3.0", // v3.0.0
        "sensio/generator-bundle": "~2.3", // v2.3.4
        "incenteev/composer-parameter-handler": "~2.0", // v2.1.0
        "doctrine/doctrine-fixtures-bundle": "~2.2.0", // v2.2.0
        "ircmaxell/password-compat": "~1.0.3", // 1.0.3
        "phpunit/phpunit": "~4.1" // 4.1.0
    }
}