Recently I’ve been tasked with refactoring and modernizing some legacy applications, and I was looking for a way to ensure that any changes would not break the functionality of existing APIs. Also, given the complete lack of any documentation for those APIs, I decided to implement a suite of “behavioral” API tests.

Behat to the Rescue

Behat is the PHP implementation of the Cucumber project for Behavioral Driven Development. It uses the Gherkin syntax to define functionality in the form of user stories and scenarios. I settled on behavioral testing and Behat for several reasons:

  1. Test scenarios are written in human-readable syntax so they can be understood by other stakeholders
  2. Once supporting code is in place, additional test scenarios can be written by anyone, not just developers
  3. Test scenarios also serve as documentation
  4. The application itself was written in PHP

Getting Started

Create a new project and install some libraries. We’ll need behat itself, an assertion library (I’ve chosen phpunit) and an http client to make calls to the API.

composer require behat/behat phpunit/phpunit guzzlehttp/guzzle

Behat tests are written using the human-readable Gherkin language in “feature” files where you define scenarios that describe how the feature will be used. Scenarios are composed of a number of sentences that are the testing steps for that scenario.

When Behat runs a feature file it loads a php class called a feature context, and matches each sentence to a method in the feature context which has the php code to support that test step.

Initialize a new behat project in your project directory. This will create an empty file, features/bootstrap/FeatureContext.php, that we’ll use to support our test steps.

vendor/bin/behat --init

A Basic Feature

Now we’re ready to write the first feature…

# features/pdizz.github.io.feature
Feature: pdizz.github.io site
    As a developer
    I need a blog
    so I can tell people about stuff

    Scenario: I visit the home page
        When I request the home page
        Then I should get a successful response

Now for the code. We can use the __construct() method to set up our http client. Then we need methods to support our first two sentences When I request the home page and Then I should get a successful response. Behat uses regex to match the sentence in the feature file with annotations in the FeatureContext docblocks. Our FeatureContext.php should look something like this:

 
<?php

use Behat\Behat\Context\Context;
use Behat\Gherkin\Node\PyStringNode;
use Behat\Gherkin\Node\TableNode;
use PHPUnit\Framework\Assert;

/**
 * Defines application features from the specific context.
 */
class FeatureContext implements Context
{
    protected $httpClient;

    /** @var \GuzzleHttp\Psr7\Request */
    protected $request;

    /** @var \GuzzleHttp\Psr7\Response */
    protected $response;

    /**
     * Initializes context.
     */
    public function __construct()
    {
        $this->httpClient = new GuzzleHttp\Client();
    }

    /**
     * @Given /^I request the home page$/
     */
    public function iRequestTheHomePage()
    {
        try {
            $this->response = $this->httpClient->get('http://pdizz.github.io/');
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $this->request = $e->getRequest();
            if ($e->hasResponse()) {
                $this->response = $e->getResponse();
            }
        }
    }

    /**
     * @Then /^I should get a successful response$/
     */
    public function iShouldGetASuccessfulResponse()
    {
        Assert::assertNotNull(
            $this->response,
            'Request did not receive any response, unable to get status code.'
        );

        $code = $this->response->getStatusCode();
        Assert::assertEquals(200, $code, "Unexpected response code: $code");
    }
}

Notice the annotations in the docblocks for iRequestTheHomePage() and iShouldGetASuccessfulResponse(). Annotations should start with one of the keywords @Given, @When or @Then and step definitions should start with Given, When, Then, And, or But. These words are interchangeable for readability, so the sentence When I request the home page will still match up with @Given /^I request the home page$/

Let’s try running the feature

pete@pete-laptop:~/Projects/pdizz_behat$ vendor/bin/behat
Feature: pdizz.github.io site
    As a developer
    I need a blog
    so I can tell people about stuff

  Scenario: I visit the home page           # features/pdizz.github.io.feature:6
    When I request the home page            # FeatureContext::iRequestTheHomePage()
    Then I should get a successful response # FeatureContext::iShouldGetASuccessfulResponse()

1 scenario (1 passed)
2 steps (2 passed)
0m0.11s (9.70Mb)

Parameters in Step Definitions

We have a couple steps defined, but they are not very re-usable in their current state. We can add parameters to our step definitions to make them much more useful. Let’s modify iRequestTheHomePage() first. We need to change the annotation to use regex to capture the parts of the sentence we want to be parameterized, the url in this case, and change the method signature to include the $url parameter.

Likewise we can change iShouldGetASuccessfulResponse() to accept the expected http status code as a paramter.

    Scenario: I visit the home page
        When I request "http://pdizz.github.io/"
        Then I should get a "200" response
<?php
...
    /**
     * @Given /^I request "(.+)"$/
     */
    public function iRequest($url)
    {
        try {
            $this->response = $this->httpClient->get($url);
        } catch (\GuzzleHttp\Exception\RequestException $e) {
            $this->request = $e->getRequest();
            if ($e->hasResponse()) {
                $this->response = $e->getResponse();
            }
        }
    }

    /**
     * @Then /^I should get a "(.+)" response$/
     */
    public function iShouldGetAResponse($expectedCode)
    {
        Assert::assertNotNull(
            $this->response,
            'Request did not receive any response, unable to get status code.'
        );

        $actualCode = $this->response->getStatusCode();
        Assert::assertEquals(
            $expectedCode,
            $actualCode,
            "Unexpected response code: $actualCode"
        );
    }
}

Now our step definitions can be used to write other scenarios. Let’s try it. I’ll write a scenario to cover this article using the step definitions we already have:

    Scenario: I view the Behat article
        When I request "http://pdizz.github.io/testing/2017/03/22/behavioral-api-testing-with-behat-01.html"
        Then I should get a "200" response

I’ve created a behat demo project to go along with this series if you want to follow along github.com/pdizz/pdizz_behat. In the next article we’ll expand on our step definitions and see some more techniques useful in testing different types of API’s.