@TheKeyboard

Simple API Testing Using Behat

| Comments

I’ve recently started using Behat as my testing framework of choice. Why? I like the idea of writing tests in a format that is as close to English as I can get. This also makes it easier for non-technical people to write tests for you as well, making sure they “have some skin in the game” as you build out your automated test suite.

I have been trying to push my JavaScript skills lately by first playing around with Node.js to get a feel for event-driven server-side coding and by trying to build something semi-useful using Knockout. Knockout is a client-side Model-View-View-Model framework that promotes four really good principles: declarative bindings, automatic UI refreshing, dependency tracking and templating. Check out the documentation on the Knockout web site for a great explanation of why those are good thigns.

A typical Knockout-out powered site is one where the client-side code talks to a server-side API. My experimental site for this is one that uses an API written in PHP using Slim. Of course, I cannot do this without writing some tests. My first set of tests are for the calling of the API itself.

After working with PHPUnit’s “assert that the result matches expectations” philosophy, switching to using a behavioural approach has changed how I decide to approach actually testing something. Let me show you what I mean.

Here’s a test for an API call to get our “current” set of transactions, which is hard-coded in the system to be all trades that have occurred in the past 3 weeks.

1
2
3
4
5
6
7
8
9
10
Feature: transactions API 

Scenario: 
  Given I call "/transactions/current"
  Then I get a response
  And the response is JSON
  And the response contains at least one transaction
  And the first transaction contains a transaction ID
  And the first transaction contains two teams
  And the first transaction contains a description

If you read that out loud it sounds like how you would describe to a co-worker what you were expecting to happen on an API call, albeit a little more stilted and formalized.

When you first write a feature like this and then ask Behat to run all the tests on it, if there is no code to execute a specific step it will give you the skeleton of code to make it happen. Then it is up to you to implement the actual code that will make the tests run.

Here’s the final product:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
<?php

use Behat\Behat\Context\ClosuredContextInterface,
    Behat\Behat\Context\TranslatedContextInterface,
    Behat\Behat\Context\BehatContext,
    Behat\Behat\Exception\PendingException;
use Behat\Gherkin\Node\PyStringNode,
    Behat\Gherkin\Node\TableNode;

//
// Require 3rd-party libraries here:
//
//   require_once 'PHPUnit/Autoload.php';
//   require_once 'PHPUnit/Framework/Assert/Functions.php';
//

/**
 * Features context.
 */
class FeatureContext extends BehatContext
{
    /**
     * Initializes context.
     * Every scenario gets it's own context object.
     *
     * @param   array   $parameters     context parameters (set them up through behat.yml)
     */
    public function __construct(array $parameters)
    {
        // Initialize your context here
    }

    /**
     * @Given /^I call "([^"]*)"$/
     */
    public function iCall($argument1)
    {
        $client = new Guzzle\Service\Client();
        $request = $client->get('http://local.ibl-delphi' . $argument1)->send();
        $this->response = $request->getBody(true);
    }

    /**
     * @Then /^I get a response$/
     */
    public function iGetAResponse()
    {
        if (empty($this->response)) {
            throw new Exception('Did not get a response from the API');
        }
    }

    /**
     * @Given /^the response is JSON$/
     */
    public function theResponseIsJson()
    {
        $data = json_decode($this->response);

        if (empty($data)) {
            throw new Exception("Response was not JSON\n" . $this->response);
        }
    }

    /**
     * @Given /^the response contains at least one transaction$/
     */
    public function theResponseContainsAtLeastOneTransaction()
    {
        $data = json_decode($this->response);

        if (count($data) < 1) {
            throw new Exception("Response did not contain at least one transaction");
        }
    }

    /**
     * @Given /^the first transaction contains a transaction ID$/
     */
    public function theFirstTransactionContainsATransactionId()
    {
        $data = json_decode($this->response, true);
        $transaction = $data[0];

        if (!isset($transaction['id'])) {
            throw new Exception("First transaction did not contain a transaction id");
        }
    }

    /**
     * @Given /^the first transaction contains two teams$/
     */
    public function theFirstTransactionContainsTwoTeams()
    {
        $data = json_decode($this->response, true);
        $transaction = $data[0];

        if (!isset($transaction['tradePartner1']) && !isset($transaction['tradePartner2'])) {
            throw new Exception("First transaction did not contain two teams");
        }
    }

     /**
     * @Given /^the first transaction contains a description$/
     */
    public function theFirstTransactionContainsADescription()
    {
        $data = json_decode($this->response, true);
        $transaction = $data[0];

        if (!isset($transaction['description'])) {
            throw new Exception("First transaction is missing a description");
        }
    }
}

Notice how Behat uses annotations that describe what type of scenario the method uses. It also uses regular expressions to extract specific values that you are wishing to test. Those are typically values inside quotes in the actual features.

If you are wondering what the skeletons (or stubbs depending on what terminology you like to use) looked like before I wrote code to implement them, you can simply cut-and-paste the scenario file at the top of this post into a feature file (don’t forget to give a .feature extension) and it will show you all of them.

I liked this method because it meant I could easily add more tests and Behat would tell me what code woudl be needed to run it.

So what does the output look like for the test?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
-> % bin/behat
Feature: transactions API

  Scenario:                                             # features/transactions.feature:3
    Given I call "/transactions/current"                # FeatureContext::iCall()
    Then I get a response                               # FeatureContext::iGetAResponse()
    And the response is JSON                            # FeatureContext::theResponseIsJson()
    And the response contains at least one transaction  # FeatureContext::theResponseContainsAtLeastOneTransaction()
    And the first transaction contains a transaction ID # FeatureContext::theFirstTransactionContainsATransactionId()
    And the first transaction contains two teams        # FeatureContext::theFirstTransactionContainsTwoTeams()
    And the first transaction contains a description    # FeatureContext::theFirstTransactionContainsADescription()

1 scenario (1 passed)
7 steps (7 passed)
0m0.362s

I have obviously only scratched the surface of what can be done with Behat. I myself have questions about how to prevent the FeatureContext.php file, which contains all the functionality that drives the tests, from getting too large. I can also see the lure of endlessly tweaking the scenarios to reduce the amount of code required to run them all.

What about code-coverage reports? Easily available in PHPUnit but is it possible using Behat? It’s still early days for some of the alternatives to PHPUnit.

The example above is only the first of numerous tests that will be written for this side project, as it needs to cover displaying archived transactions to be voted on, user authentication, and the voting process itself.

I think that the BDD approach maps better to a results-oriented methodology when it comes to testing: you are using real-life scenarios to make sure that you are getting back the results you are expecting. In the end. that is still the goal of testing.

Comments