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-10-16 weaverryan

    Awesome! And I just added a link to it down in our tip for this section :) - https://knpuniversity.com/scre... - I'm sure it will be useful for others!

    Thanks!

  • 2016-10-16 Johan

    I think I added most of the features now. I haven't tested all of them yet but I will fix bugs as I encounter them.

  • 2016-10-15 Johan

    I decided to just begin rewriting the file using the latest version of Guzzle (6.2) and Behat (3.2). I will be moving and rewriting the functions as I need them.

    I set up a git repository for it if you would be interested: https://github.com/thejager/be...

    Thanks :)

  • 2016-10-15 weaverryan

    Hey Johan!

    There's not currently an updated version of ApiFeatureContext. There are two major version things that are important if you wanted to use it with the latest and greatest:

    1) The version of Guzzle - it's 3.7 in this project and the latest is 6.0. That would require a good number of changes. However, in our Symfony REST tutorial, the first episodes use Guzzle 3.7 and the later ones use Guzzle 6. You can see the differences by comparing the ApiTestCase in episode 1 (knpuniversity.com/screencast/s... with episode 4 (http://knpuniversity.com/scree....

    2) The version of Behat is 2.5 in the tutorial and the latest is 3. This is really not a huge upgrade (and we have a Behat v2.5 tutorial here and a Behat v3 tutorial) and there are some details here: https://github.com/Behat/Behat...

    We don't have plans right now to upgrade this tutorial to the latest stuff, but if you're interested in trying to upgrade the ApiFeatureContext class for the latest version of these libraries, I'd be very happy to help answer any questions or help you debug any errors you have. Ultimately, I think this would be helpful to others as well.

    Cheers!

  • 2016-10-15 Johan

    Is there a (maintained) composer package for this ApiFeatureContext class? I tried to integrate this into my new symfony 3 project and it gives tons of errors. I want to use it :(

    I tried two other behat API extension packages but they don't seem nearly as complete.

  • 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;