Buy

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!

  • 2016-04-13 weaverryan

    Ah, I'm glad you posted this! The hardcoding is done on purpose. Part of what you are testing is that the URL to your page is /register. If that ever changed, you *would* want your tests to fail (perhaps you accidentally changed the URL of the route). Not everyone does this, but generally speaking, it is the best practice to hard code URLs in your test.

    Cheers!

  • 2016-04-09 Ленур

    $client->request('GET', '/register'); - this is hard code.
    I use $this->parameters->get('router')->generate($route, $params) - where $route - route name, $params - params to route.

  • 2016-02-25 weaverryan

    Hey Matt!

    Try just: vendor/bin/behat

    So, *without* the php part. That's the correct way to do it in Windows - I should have used that more portable format for this tutorial and we use that in newer ones. That should work for you :).

    Cheers!

  • 2016-02-24 matt

    when i type php vendor/bin/behat it just prints out the behat file

    dir=$(d=${0%[/\\]*}; cd "$d"; cd "../behat/behat/bin" && pwd)

    # See if we are running in Cygwin by checking for cygpath program

    if command -v 'cygpath' >/dev/null 2>&1; then

    # Cygwin paths start with /cygdrive/ which will break windows PHP,

    # so we need to translate the dir path to windows format. However

    # we could be using cygwin PHP which does not require this, so we

    # test if the path to PHP starts with /cygdrive/ rather than /usr/bin

    if [[ $(which php) == /cygdrive/* ]]; then

    dir=$(cygpath -m $dir);

    fi

    fi

    dir=$(echo $dir | sed 's/ /\ /g')

    "${dir}/behat" "$@"

    Anyone have any idea why? it does the same for phpunit

  • 2016-02-14 Arturas Lapiskas

    i have problems running phpunit on windows, to run you must enter command without php in front:
    cd bin
    phpunit -c ../app/

  • 2015-09-10 weaverryan
  • 2015-09-10 guest

    still typo here
    // src/Yoda/UserBundle/Tests/Controller/RegisterControllerTest.php
    namespace Yoda\EventBundle\Tests\Controller;

  • 2015-07-10 guest

    namespace Yoda\UserBundle\Tests\Controller;