Flag of Ukraine
SymfonyCasts stands united with the people of Ukraine
This course is archived!
This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

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

  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!

32
Login or Register to join the conversation
Carlos Avatar

And now in 2020 (almost 2021), for testing APIs.... what is better, behat or PHPSpec?

Reply

Hey Carlos!

In 2020/2021, I am using Symfony's native testing tools (also, if you use API Platform, they have some nice add-ons onto the testing tools). Behat is really interesting, but I think, for most people, it's probably a bit overkill for API's. So, use PHPUnit with whatever "test client" your framework gives you :).

Cheers!

Reply
Carlos Avatar

Thanks!!

Reply
Ana G. Avatar
Ana G. Avatar Ana G. | posted 4 years ago | edited

Hi

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?
Reply

Hi Ana G.!

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

Cheers!

Reply
Ana G. Avatar

Yes! I'm using PHP 7.2

Thank you very much!

Reply
Default user avatar

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

Reply

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!

Reply
Default user avatar

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

Reply

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: https://knpuniversity.com/s...
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.

Cheers!

Reply
Rajaona F. Avatar
Rajaona F. Avatar Rajaona F. | posted 5 years ago

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

Reply

Hey Rajaona F.

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

Cheers!

Reply
Rajaona F. Avatar

It's works thanks .... :)

Reply
Default user avatar
Default user avatar toporovvv | posted 5 years ago

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

Reply

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

Cheers!

1 Reply
Default user avatar

namespace Yoda\UserBundle\Tests\Controller;

Reply
Default user avatar

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

Reply
Default user avatar

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.

Reply

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/screencas... with episode 4 (http://knpuniversity.com/sc....

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

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!

Reply
Default user avatar

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

Thanks :)

1 Reply
Default user avatar

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.

Reply

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

Thanks!

Reply
Default user avatar
Default user avatar Arturas Lapinskas | posted 5 years ago

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

Reply
Default user avatar
Default user avatar Boran Alsaleh | posted 5 years ago

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 !

Reply

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');
}

Cheers!

1 Reply
Default user avatar

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

Reply

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!

Reply
Florian Avatar
Florian Avatar Florian | weaverryan | posted 4 years ago | edited

Hello weaverryan,

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

Cheers

Reply

Hey Florian

Is the call command a PHPStorm thing?

Reply
Rainer-S Avatar
Rainer-S Avatar Rainer-S | MolloKhan | posted 4 years ago | edited

Hey MolloKhan,
in PHPStorm the <u>cmd.exe</u> will be called by default.
If you'd like to call the behat.bat, the <i>call</i> command is necessary.

Command documentation:
https://docs.microsoft.com/en-us/windows-server/administration/windows-commands/call

If you are using the Windows PowerShell, you don't need the <i>call</i> 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

Cheers

Reply

Ohh that's great info! Thanks for sharing it Rainer S.

Cheers!

1 Reply
Cat in space

"Houston: no signs of life"
Start the conversation!

This tutorial uses a deprecated micro-framework called Silex. The fundamentals of REST are still ? valid, but the code we use can't be used in a real application.

What PHP libraries does this tutorial use?

// composer.json
{
    "require": {
        "silex/silex": "~1.0", // v1.3.2
        "symfony/twig-bridge": "~2.1", // v2.7.3
        "symfony/security": "~2.4", // v2.7.3
        "doctrine/dbal": "^2.5.4", // v2.5.4
        "monolog/monolog": "~1.7.0", // 1.7.0
        "symfony/validator": "~2.4", // v2.7.3
        "symfony/expression-language": "~2.4" // v2.7.3
    },
    "require-dev": {
        "behat/mink": "~1.5", // v1.5.0
        "behat/mink-goutte-driver": "~1.0.9", // v1.0.9
        "behat/mink-selenium2-driver": "~1.1.1", // v1.1.1
        "behat/behat": "~2.5", // v2.5.5
        "behat/mink-extension": "~1.2.0", // v1.2.0
        "phpunit/phpunit": "~5.7.0", // 5.7.27
        "guzzle/guzzle": "~3.7" // v3.9.3
    }
}
userVoice