Behat for testing RESTful APIs

Posted: 2014-08-13 19:11:47

The code for behat's FeatureContext comes from this repo and book https://github.com/philsturgeon/build-apis-you-wont-hate

I will bring it together so before long you can run tests like this

test

The road map will be

  • Quick Setup Notes
  • GET requests
  • POST request
  • Tie into your framework
  • Seeding step
  • Query your db step
  • PUT request

Quick Setup Notes

Setting up behat is beyond the scope of this. If you want try alnutile/behat-seed to get going on testing since it can be used to hit any API. But using the FeatureContext file from the https://github.com/philsturgeon/build-apis-you-wont-hate instead. So with that and his composer.json info we are ready to build out our testing environment.

With that setup we are using composer to pull in all the packages and now have guzzle at our disposal.

Also the behat.yml file needs the default context parameters setup.

default:
  paths:
    features:  behat/features
    bootstrap: features/bootstrap
  context:
    parameters:
      base_url: http://local.dev

Sometimes I would not set this since I do mostly selenium work.

GET Request

To start with we will test using GET, a very simple start.

Feature: Projects
  This projects data
  As an authenticated user
  I should be able to see all the projects of my team

  Scenario: I should see projects output
    Given I reseed the database
    When I request "GET /api/v1/projects"
    Then I get a "200" response
    And data has 5 items

That is it! Behat will not hit that endpoint using Guzzle and do a get request. We test the response and then count the results.

Here is the count code

    /**
     * @Given /^data has (\d+) items/
     */
    public function dataHasItems($arg)
    {
        $results = $this->getResponsePayload();
        assertCount($arg, $results->data);
    }

POST Request

This got a bit trickier

The final result looked like this

Scenario: Creating a new Project and new Site
    Given I reseed the database
    Given I have the payload:
      """
      { "data":
        {
           "name": "test foo",
           "branch": "test",
           "folder": "foo",
           "active": 1,
           "team_id": "foo-bar-foo-baz-5",
           "site_id": "test-foo-site-id-test-new",
           "urls": [
            {"name": "Url 1 Behat", "path": "http://foo1.behat" },
            {"name": "Url 2 Behat", "path": "http://foo2.behat" }
           ],
           "site_object": {
             "id": "test-foo-site-id-test-new",
             "name": "Site via Behat",
             "repo_name": "foo_repo",
             "active": 1
           },
           "team_object": {
             "id": "foo-bar-foo-baz-5",
             "name": "Team 5"
           }
         }
       }
       """
    When I request "POST /api/v1/projects"
    Then I get a "200" response
    When I request "GET /api/v1/projects"
    And data has 6 items
    And there are 6 rows of "\BehatEditor\Models\Site"

Lets cover it one line or so at a time

Given I reseed the database

Sometimes I want to start with a new data set and this is what triggers it. I will cover this more in the sections below "Tie into your framework" and "Seeding step"

Now the payload

Given I have the payload:
      """
      { "data":
        {
           "name": "test foo",
           "branch": "test",
           "folder": "foo",
           "active": 1,
           "team_id": "foo-bar-foo-baz-5",
           "site_id": "test-foo-site-id-test-new",
           "urls": [
            {"name": "Url 1 Behat", "path": "http://foo1.behat" },
            {"name": "Url 2 Behat", "path": "http://foo2.behat" }
           ],
           "site_object": {
             "id": "test-foo-site-id-test-new",
             "name": "Site via Behat",
             "repo_name": "foo_repo",
             "active": 1
           },
           "team_object": {
             "id": "foo-bar-foo-baz-5",
             "name": "Team 5"
           }
         }
       }
       """

The API expects the payload to be in the data object. From there it is just a object of data relative to the Project endpoint. We will see too this gets pretty cool cause we will make sure new sites are made, new urls are made etc as needed.

When I request "POST /api/v1/projects"
Then I get a "200" response

Then we post it. Phil's code was modified a bit here

    /**
     * @When /^I request "(GET|PUT|POST|DELETE) ([^"]*)"$/
     */
    public function iRequest($httpMethod, $resource)
    {
        $this->resource = $resource;

        $method = strtolower($httpMethod);

        try {
            switch ($httpMethod) {
                case 'PUT':
                    $this->response = $this
                        ->client
                        ->$method($resource, null, $this->requestPayload);
                    break;
                case 'POST':
                    $post = \GuzzleHttp\json_decode($this->requestPayload, true);
                    $this->response = $this
                        ->client
                        ->$method($resource, array('body' => $post));
                    break;
                default:
                    $this->response = $this
                        ->client
                        ->$method($resource);
            }
        } catch (BadResponseException $e) {

            $response = $e->getResponse();

            // Sometimes the request will fail, at which point we have
            // no response at all. Let Guzzle give an error here, it's
            // pretty self-explanatory.
            if ($response === null) {
                throw $e;
            }

            $this->response = $e->getResponse();
        }
    }

I added the post line (hmm need to pull out the json_decode just noticed that) anyways we send this off to Guzzle's post method to send to the API.

Basically I added this

case 'POST':
                    $post = \GuzzleHttp\json_decode($this->requestPayload, true);
                    $this->response = $this
                        ->client
                        ->$method($resource, array('body' => $post));
                    break;

As you see above.

When I request "GET /api/v1/projects"
And data has 6 items
And there are 6 rows of "\BehatEditor\Models\Site"

This is to verify new items where made. The last line is to verify that a new Site was made since it does not have a restful endpoint to GET a count nor POST since it is only part of the Project and from an external API. The step for that is

    /**
     * @Given /^there are (\d+) rows of "([^"]*)"$/
     */
    public function thereAreRowsOf($arg1, $arg2)
    {
        $count = $arg2::all()->count();
        if($count != $arg1) {
            throw new Exception(
                "Actual count is:\n" . $count
            );
        }
    }

We pass in the namespace of the Model and the expected count and then run the query. More on that in the integration area.

Tie into your framework

For this project, Silex, it was quite easy. In the FeatureContext class I do this following making $this->core the app.

    public function __construct(array $parameters)
    {
        $config = isset($parameters['guzzle']) && is_array($parameters['guzzle']) ? $parameters['guzzle'] : [];

        $config['base_url'] = $parameters['base_url'];

        $this->client = new Client($config);

        $this->core = require_once __DIR__.'/../../../../bootstrap/start.php';

    }

I basically pull in the boostrap file with all the info to setup the database, methods etc used by the index.php and command line tools. Then later on I can do queries of the db and other things that really need to tie into the framework. Like

    /**
     * @Given /^there are (\d+) rows of "([^"]*)"$/
     */
    public function thereAreRowsOf($arg1, $arg2)
    {
        $count = $arg2::all()->count();
        if($count != $arg1) {
            throw new Exception(
                "Actual count is:\n" . $count
            );
        }
    }

This takes the request

And there are 6 rows of "\BehatEditor\Models\Site"

And queries the db using the Model class to do a simple all()->count() on the results.

Seeding

Now that things are tied in I made a simple step for this

    /**
     * @Given /^I reseed the database$/
     */
    public function iReseedTheDatabase()
    {
        if($this->core->getEnv() != 'production') {
          $path = __DIR__.'/../../../../' . 'setup/seed.php';
          exec("php $path");
        }
    }

PUT Coming soon

Delete Coming Soon

Auth Coming soon


Tags:

behat php