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 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.


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.


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


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
    $this->assertEquals('Leia', $usernameVal);

Re-run the test to see the result:

php bin/phpunit -c app


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

Leave a comment!

  • 2019-07-24 Diego Aguiar

    Ohh that's great info! Thanks for sharing it Sven


  • 2019-07-23 Sven

    Hey Diego Aguiar,
    in PHPStorm the cmd.exe will be called by default.
    If you'd like to call the behat.bat, the call command is necessary.

    Command documentation:

    If you are using the Windows PowerShell, you don't need the call command and it is also not supported.

    In short:
    WindowsPowershell command = vendor/bin/behat.bat
    cmd.exe command = call vendor/bin/behat.bat

    In the PHPStorm settings under Tools->Terminal it is also possible to change the cmd.exe into the powershell.exe


  • 2019-07-22 Diego Aguiar

    Hey Sven

    Is the call command a PHPStorm thing?

  • 2019-07-22 Sven

    Hello weaverryan,

    I have figured the windows command for the PHP-Storm Terminal out.
    It is:
    call vendor/bin/behat.bat


  • 2018-09-21 Ana Guerra

    Yes! I'm using PHP 7.2

    Thank you very much!

  • 2018-09-18 weaverryan

    Hi Ana Guerra!

    Hmm. I did some digging, and it looks like this is caused by a small bug/outdated code in PhpUnit + PHP 7.2 (are you using PHP 7.2?). If the errors don't affect your tests, I'd ignore them for the purposes of learning this tutorial :). If they *are*, try updating phpunit. It's a few steps, as our phpunit has gotten a little bit out of date ;).

    1) change the phpunit/phpunit version constraint in composer.json to ^6.0.0.

    2) run composer update phpunit/*

    3) In features/api/ApiFeatureContext.php, remove the two require_once lines, and replace them with only this one:

    require_once __DIR__.'/../../vendor/phpunit/phpunit/src/Framework/Assert/Functions.php';

    You mentioned that your tests *were* passing despite the errors - but I wanted to give the above steps... just in case ;).


  • 2018-09-18 Ana Guerra


    The tests in programmer.feature passed but also printed this lines:

    PHP Fatal error: Declaration of PHPUnit_Framework_Comparator_DOMDocument::assertEquals($expected, $actual, $delta = 0, $canonicalize = false, $ignoreCase = false) must be compatible with PHPUnit_Framework_Comparator_Object::assertEquals($expected, $actual, $delta = 0, $canonicalize = false, $ignoreCase = false, array &$processed = Array) in /var/www/knp-rest/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/DOMDocument.php on line 114

    PHP Fatal error: Declaration of PHPUnit_Framework_Comparator_DOMDocument::assertEquals($expected, $actual, $delta = 0, $canonicalize = false, $ignoreCase = false) must be compatible with PHPUnit_Framework_Comparator_Object::assertEquals($expected, $actual, $delta = 0, $canonicalize = false, $ignoreCase = false, array &$processed = Array) in /var/www/knp-rest/vendor/phpunit/phpunit/PHPUnit/Framework/Comparator/DOMDocument.php on line 114

    Could you help me?

  • 2018-03-13 Victor Bocharsky

    Hey Boran,

    Yes sure! Well, it's a bit strange that you have the same data from different URLs, are you sure you need both those API endpoints? Anyway, I think it's easy to achieve. Looks like you're talking about integration test, so you just need to send requests to those URLs, store both response to variables with your favorite HTTP client and then iterate one variable and use any PHPUnit's assert function that fits best for you to make sure the values you iterate are equal to the values from the 2nd variable, i.e. something like this:

    // Check that both response data have the same number of elements
    $this->assertCount(count(response1), response2);
    // Iterate over 1st response data and check some values are matched the values from the 2nd response
    foreach ($response1 as $key => $value) {
    $this->assertEquals($value['title'], $response2[$key]['title'], 'Any useful message for you here which helps you to better understand was was wrong with this assertion');


  • 2018-03-11 Boran Alsaleh

    Hi , I have 2 API's (2 URL ) and each URL takes id as a parameter and return json data , and I have an array contains a lot of id's , I want to write a test that reads this array (loop) and asserts , that the data from Both Url is Same when I send the same Id as parameter , and after that , it should give me which Id's are failed to assert same data ,How could I do it with phpunit framework ?!!!
    THanks !

  • 2017-10-04 RAJAONA Ywoumé

    It's works thanks .... :)

  • 2017-10-04 Diego Aguiar

    Hey RAJAONA Ywoumé

    Look's like you need to install php mbstring to your working server.
    You can install it like this: (Or look for how to install it for your specific OS)

    $ apt-get install php-mbstring


  • 2017-10-04 RAJAONA Ywoumé

    PHP Fatal error: Uncaught Error: Call to undefined function Behat\Behat\DependencyInjection\mb_internal_encoding() .... why ????

  • 2017-07-04 Victor Bocharsky

    Hey Lily,

    We're talking about Behat tests, right? Try to find failed step definition, it should be red if you have colors in your terminal. Also, you can print the last response right *before* the failed step - check it out here:
    It will print you the response and you can debug the problem.

    Also, feel free to use "dump($someVar)" and "die()" inside step definitions to print some helpful debug information in your terminal and stop further execution to understand the problem.


  • 2017-06-30 Lily

    What should I do if my test failed? How should I go about finding what the problem is using the printed debugging info? Thanks~

  • 2017-04-27 Diego Aguiar

    It's the eye of the tiger my friend ;)


  • 2017-04-27 toporovvv

    OMG! Does anyone heard that beautiful sounds on 3:26? :)

  • 2016-10-16 weaverryan

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


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

    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 ( with episode 4 (

    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:

    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.


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


  • 2016-04-09 Lenur

    $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 :).


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



    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

    Fixed at Thanks!

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