If you liked what you've learned so far, dive in!
Subscribe to get access to this tutorial plus
video, code and script downloads.
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.
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.
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.
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.
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!
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!
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!
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?
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!
$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.
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!
What should I do if my test failed? How should I go about finding what the problem is using the printed debugging info? Thanks~
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!
PHP Fatal error: Uncaught Error: Call to undefined function Behat\Behat\DependencyInjection\mb_internal_encoding() .... why ????
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!
still typo here
// src/Yoda/UserBundle/Tests/Controller/RegisterControllerTest.php
namespace Yoda\EventBundle\Tests\Controller;
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.
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!
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 :)
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.
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!
i have problems running phpunit on windows, to run you must enter command without php in front:
cd bin
phpunit -c ../app/
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 !
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!
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
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!
Hello weaverryan,
I have figured the windows command for the <b>PHP-Storm Terminal</b> out.
It is:call vendor/bin/behat.bat
Cheers
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
// 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
}
}
And now in 2020 (almost 2021), for testing APIs.... what is better, behat or PHPSpec?