Behat for 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

Behat for Testing

The great thing about using PHPUnit is that it’s dead-simple: make an HTTP request and assert some things about its response. If you want to test your APIs using Guzzle and PHPUnit, you’ll be very successful and your office will smell of rich mahogany.

But in our app, we’re going to make our tests much more interesting by using a tool called Behat. If you’re new to Behat, you’re in for a treat! But also don’t worry: we’re going to use Behat, but not dive into it too deeply. And when you want to know more, watch our Behat Screencast and then use the code that comes with this project to jumpstart testing your API.

Creating Scenarios

With Behat, we write human-readable statements, called scenarios, and run these as tests. To see what I mean, find the features/api/programmer.feature file:

# api/features/programmer.feature
Feature: Programmer
  In order to battle projects
  As an API client
  I need to be able to create programmers and power them up

    # Given the user "weaverryan" exists

  Scenario: Create a programmer

As you’ll see, each feature file will contain many scenarios. I’ll fill you in with more details as we go. For now, let’s add our first scenario: Create a Programmer:

# api/features/programmer.feature
# ...

Scenario: Create a programmer
  Given I have the payload:
      "nickname": "ObjectOrienter",
      "avatarNumber" : "2",
      "tagLine": "I'm from a test!"
  When I request "POST /api/programmers"
  Then the response status code should be 201
  And the "Location" header should be "/api/programmers/ObjectOrienter"
  And the "nickname" property should equal "ObjectOrienter"

I’m basically writing a user story, where our user is an API client. This describes a client that makes a POST request with a JSON body. It then checks to make sure the status code is 201, that we have a Location header and that the response has a nickname property.

Running Behat

I may sound crazy, but let’s execute these english sentences as a real test. To do that, just run the behat executable, which is in the vendor/bin directory:

$ php vendor/bin/behat

Green colors! It says that 1 scenario passed. In the background, a real HTTP request was made to the server and a real response was sent back and then checked. In our browser, we can actually see the new ObjectOrienter programmer.

Configuring Behat

Oh, and it knows what our hostname is because of a config file: behat.yml.dist. We just say POST /api/programmers and it knows to make the HTTP request to http://localhost:8000/api/programmers.


If you’re running your site somewhere other than localhost:8000, copy behat.yml.dist to behat.yml and modify the base_url in both places.

How Behat Works

Behat looks like magic, but it’s actually really simple. Open up the ApiFeatureContext file that lives in the features/api directory. If we scroll down, you’ll immediately see functions with regular expressions above them:

// features/api/ApiFeatureContext.php
// ...

 * @When /^I request "(GET|PUT|POST|DELETE|PATCH) ([^"]*)"$/
public function iRequest($httpMethod, $resource)
    // ...

Behat reads each line under a scenario and then looks for a function here whose regular expression matches it. So when we say I request "POST /api/programmers", it calls the iRequest function and passes POST and /api/programmers as arguments. In there, our old friend Guzzle is used to make HTTP requests, just like we’re doing in our testing.php script.


Hat-tip to Phil Sturgeon and Ben Corlett who originally created this file for Phil’s Build APIs you Won’t Hate book.

Also, a KnpU (Johan de Jager) user has ported the ApiFeatureContext to work with Guzzle 6 and Behat 3. You can find it here:

To sum it up: we write human readable sentences, Behat executes a function for each line and those functions use Guzzle to make real HTTP requests. Behat is totally kicking butt for us!

Seeing our Library of Behat Sentences

I created this file and filled in all of the logic in these functions. This gives us a big library of language we can use immediately. To see it, run the same command with a -dl option:

$ php vendor/bin/behat -dl

Anywhere you see the quote-parentheses mess that’s a wildcard that matches anything. So as long as we write scenarios using this language, we can test without writing any PHP code in ApiFeatureContext. That’s powerful.

If you type a line that doesn’t match, Behat will print out a new function with a new regular expression. It’s Behat’s way of saying “hey, I don’t have that language. So if you want it, paste this function into ApiFeatureContext and fill in the guts yourself”. I’ve already prepped everything we need. So if you see this, you messed up - check your spelling!

And if using Behat is too much for you right now, just keep using the PHPUnit tests with Guzzle, or even use a mixture!

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;