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:
Action |
Endpoint |
Feature |
Create |
POST /users |
features/users.feature |
Read |
GET /users/X |
features/users.feature |
Update |
POST /users/X |
features/users.feature |
Delete |
DELETE /users/X |
features/users.feature |
List |
GET /users |
features/users.feature |
Image |
PUT /users/X/image |
features/users-image.feature |
Favorites |
GET /users/X/favorites |
features/users-favorites.feature |
Checkins |
GET /users/X/checkins |
features/users-checkins.feature |
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
2
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
22
23 Scenario: Listing all places is not possible
24 When I request "GET /places"
25 Then I get a "400" response
26
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
31
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.
Test. TEST. TEST YOUR APPLICATIONS.
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.