Buy

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

  Background:
    # 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.

Note

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.

Note

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: https://github.com/thejager/behat-api-feature-context.

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!

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