Endpoint Testing - Build APIs You Won't Hate: Everyone and their dog wants an API, so you should probably learn how to build them (2014)

Build APIs You Won't Hate: Everyone and their dog wants an API, so you should probably learn how to build them (2014)

5. Endpoint Testing

5.1 Introduction

You might be sitting there thinking “This really escalated quickly, I’m not ready for testing!” but this is essentially the point. You have to set up your tests as early as possible so you actually bother using them, otherwise they become the “next thing” that just never gets done. Have no fear. Testing an API is not only easy, it is actually really quite fun.

5.2 Concepts & Tools

With an API there are a few things to test, but the most basic idea is “when I request this URL, I want to see a foo resource”, and “when I throw this JSON at the API, it should a) accept it or b) freak out.”

This can be done in several ways and a lot of people will instantly try to unit-test it, but that quickly becomes a nightmare. While you might think just writing a bit of code with your favorite HTTP client is simple, if you have over 50 endpoints and want to do multiple checks per endpoint you end up with a mess of code which can become hard to maintain, especially if your favorite HTTP client releases a major version with a new interface.

The more code you have in your tests, the higher the chances of your tests being rubbish - which means you wont run them. Bad tests also run the risk of false positives, which are super dangerous as they lead you into thinking your code actually works when it does not.

One very simplistic approach will be to use a BDD (Behaviour Driven Development) tool. A very popular BDD tool is Cucumber and this is considered by many to be a Ruby tool. It can in fact be used for Python, PHP and probably a whole bevy of other languages but some of the integrations can be tricky. For the PHP users here, we will be using Behat which is pretty much the same thing, along with Gherkin (the same DSL (Domain-Specific Language) that Cucumber uses, so all of us are on basically the same page.)

The outline of this chapter will be to show how to set up and use the BDD tool Behat, talk through the various moving parts then show you a working example in our source code inside a Laravel sample app. You can build your own tests in your own language or in any framework, but just go along with this PHP example to see a basic working - even if you personally prefer another language. Go on. It wont bite.

5.3 Setup

As a PHP developer you simply need to install Behat, and this can be done with Composer. It is fair to assume that if you are using any sort of modern PHP framework you are already familiar with this so I won’t bore the non-PHP devs by getting stuck into it.

Assuming that composer is installed globally in your system, to install Behat run:

Install Behat globally with Composer

1 $ composer global require 'behat/behat=2.4.*'

otherwise run: ~~~~~~~~ $ php composer.phar global require ‘behat/behat=2.4.*’ ~~~~~~~~

In any case, make sure ~/.composer/vendor/bin/ is added to your $PATH and you should be good to go.

If you are a Ruby user you have the ease of simply running $ gem install cucumber, or shove it in your Gemfile.

Google should help you with Python.

The rest of this chapter is going to stick purely to PHP for the sake of simplicity, and others can just use the equivilent commands as we go.

5.4 Initialise

These Behat tests will live in a tests folder, but it may need to co-exist with other unit-tests or other types of test. For this reason I like to put them in a sub-folder called tests/behat.

I have provided an example of a simple Behat test suite in the sample code which lives inside the app/ folder. This is done mainly because it is a good place to put your tests and Laravel already has a tests folder, but if you are using any other framework you can put these tests anywhere you please.

So, go to the app folder:

1 $ cd ~/apisyouwonthate/chapter5/app

The folder structure and basic Behat setup has already been run with the following commands (so you can skip this step):

1 $ mkdir -p tests/behat && cd tests/behat

2 $ behat --init

This will have the following output:

1 +d features - place your *.feature files here

2 +d features/bootstrap - place bootstrap scripts and static files here

3 +f features/bootstrap/FeatureContext.php - place your feature related code here

The output here outlines the structure of files it has created. Everything lives inside the features/ folder and this will be where your Behat tests will go. The features/bootstrap/ folder contains only one file at this point, which is FeatureContext.php.

The default version of this file is a little bare so this sample code contains a beefed up one, which will be used throughout this chapter.

5.5 Features

Features are a way to group your various tests together. For me I keep things fairly simple and consider each “resource” and “sub-resource” to be its own “feature”.

Looking at our users example from Chapter 2:





POST /users



GET /users/X



POST /users/X



DELETE /users/X



GET /users



PUT /users/X/image



GET /users/X/favorites



GET /users/X/checkins


So, anything to do with /places and /places/X would be the same, but as soon as you start looking at /places/X/checkins that becomes a new feature because we are talking about something else.

You can use that convention or try something else, but this grows pretty well without having a bazillion files to sift through.

5.6 Scenarios

Gherkin uses “Scenarios” as its core structure and they each contain “steps”. In a unit-testing world the “scenarios” would be their own “methods”, and the “steps” would be “assertions”.

These Features and Scenarios line up with the “Action Plan” created in Chapter 2. Each RESTful Resource needs at least one “Feature”, and because each “Action” has an “Endpoint” we need at least one “Scenario” for each “Action”.

Too much jargon? Time for an example:

1 Feature: Places


3 Scenario: Finding a specific place

4 When I request "GET /places/1"

5 Then I get a "200" response

6 And scope into the "data" property

7 And the properties exist:

8 """

9 id

10 name

11 lat

12 lon

13 address1

14 address2

15 city

16 state

17 zip

18 website

19 phone

20 """

21 And the "id" property is an integer


23 Scenario: Listing all places is not possible

24 When I request "GET /places"

25 Then I get a "400" response


27 Scenario: Searching non-existent places

28 When I request "GET /places?q=c800e42c377881f8202e7dae509cf9a516d4eb59&lat=1&lon=1"

29 Then I get a "200" response

30 And the "data" property contains 0 items


32 Scenario: Searching places with filters

33 When I request "GET /places?lat=40.76855&lon=-73.9945&q=cheese"

34 Then I get a "200" response

35 And the "pagination" property is an object

36 And the "data" property is an array

37 And scope into the first "data" property

38 And the properties exist:

39 """

40 id

41 name

42 lat

43 lon

44 address1

45 address2

46 city

47 state

48 zip

49 website

50 phone

51 """

52 And reset scope

This uses some custom rules which have been defined in FeatureContext.php but more on that shortly.

The Feature file is called places.feature and has 4 scenarios. One to find a specific place, another to show that listing all places is not allowed (400 means bad input, your should specify lat lon) and two more to test how well searching works.

I try to think up the guard clauses that my endpoints will need, then make a “Scenario” for each of those. So, if you don’t send a lat/lon to search then it errors. Test that.

Expecting a boolean value but get a string? Test that:

1 Scenario: Wrong Arguments for user follow

2 Given I have the payload:

3 """

4 {"is_following": "foo"}

5 """

6 When I request "PUT /users/1"

7 Then I get a "400" response

Want to be sure your controllers can handle weird requests with a 404 instead of freaking out and going all 500 Internal Error? Test that.

1 Scenario: Try to find an invalid moments

2 When I request "GET /moments/nope"

3 Then I get a "404" response

Sure you don’t actually have any code yet, but you can write all of these tests based off of nothing but your “Action Plan” and your Routes. You should use what you know about the output content structure from Chapter 3 to plan what output you expect to see.

Then all you need to do is… you know… build your entire API.

5.7 Prepping Behat

You are probably wondering how you actually run these tests, because Behat involves making HTTP requests and you’ve just been writing text-files. Well, the class in FeatureContext.php handles all of that and a lot more, but first we need to configure Behat so we know what the hostname is going to be for these requests.

1 $ vim app/tests/behat/behat-dev.yml

In this file put in something along the lines of:

1 default:

2 context:

3 parameters:

4 base_url: http://localhost:80000

If you have virtual hosts set up on your machine then use those, and if you are running a local web-server on a different port then obviously you can use that too. That value could be http://localhost:4000 or http://dev-api.example.com, it does not matter.

5.8 Running Behat

This is the easiest bit:

1 $ behat -c tests/behat/behat-dev.yml

Running this from the sample application should return a lot of green lights because I have gone to the effort of writing a few very basic feature tests against a few very simple endpoints that return data from an SQLite database.

Once you have that running I recommend you try and make some tests in your own applications along the same sort of lines. While we will have sample code to play with for many chapters, I strongly suggest you try to test your own API (brand new or existing) too, as this is the most value you could get from the book.



Ongoing Testing

Soon I will try and add more complicated test examples to this chapter to show off what can be done. I will also expand the tests in later chapters as we go to cover the various features being added like Pagination and Links.

Test Driven Development

Writing tests-first is also a great way to go. Now that you have an understanding of your action plan and what the endpoints should be and what their output should look like you should be fine to build out tests again them even if they do not exist.

Running the tests will show you that everything is broken of course, then you just go through and build and test the endpoints one at a time. This sounds hard but you just cannot afford to mess about with testing on an API.

Doing this first will save you a lot of hard work down the road. I have the scars to prove it.