
Verifying Entire API Responses
Most people do a pretty lazy job of testing APIs. In my ABCs of APIs workshop, I ask people to manually test an API. When looking at a response, they typically glance at it, and maybe carefully review some of the key fields. Same goes for automated tests – only the key fields are typically asserted against.
Let’s look at this GET call:
1 |
http://api.zippopotam.us/us/20500 |
This returns the following response:
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 |
HTTP/1.1 200 OK Date: Sun, 23 Jun 2019 01:51:49 GMT Content-Type: application/json Transfer-Encoding: chunked Connection: keep-alive Set-Cookie: __cfduid=de53cb3947f3d5b8f887d2ed9514b17ce1561254709; expires=Mon, 22-Jun-20 01:51:49 GMT; path=/; domain=.zippopotam.us; HttpOnly X-Cache: hit Charset: UTF-8 Vary: Accept-Encoding Access-Control-Allow-Origin: * Server: cloudflare CF-RAY: 4eb2d1b0ff8e9647-SJC Content-Encoding: gzip { "post code": "20500", "country": "United States", "country abbreviation": "US", "places": [ { "place name": "Washington", "longitude": "-77.0355", "state": "District of Columbia", "state abbreviation": "DC", "latitude": "38.8946" } ] } |
Given this response, many unit tests will make sure the response code is 200 and that the body is not null. Functional tests may go a bit further and verify some of the key fields of the body.
Most people won’t script a test that verifies EVERY bit of this response, mostly because it’s really tedious to do and arguably some of the fields may pose a lower risk if they are incorrect.
My argument is that you don’t know how every customer will use your API, and therefore can’t be sure which of these fields are most important to them.
Because of this, I’ve been seeking a way to verify an entire response with a single assertion. I’d gotten close to being able to do this by deserializing the API response into a POJO and comparing the resulting object with an expected object. But even with this approach, I needed to code up the POJO, and build the expected object in my code. Still a bit tedious.
Luckily, Mark Winteringham taught a workshop on Approval Tests (created by Llewellyn Falco) and showed us how to verify an entire response body! I went home and played with it more on my own and am loving it!
After the first run of your test, Approval Tests will save your expected result to a file. Then on each subsequent run, it will compare the new result to what’s saved as the expected result. If the results differ, the test fails and a diffing program such DiffMerge can show you the differences between the files.
Video by Clare Macrae
Dependencies
I created a pom file with Approval Tests, TestNG (as the test runner), and Rest-Assured (as the API executor). Note that Approval Tests can work alongside any other test runner and API executor, and there’s support for several programming languages as well.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
<dependencies> <dependency> <groupId>com.approvaltests</groupId> <artifactId>approvaltests</artifactId> <version>RELEASE</version> </dependency> <dependency> <groupId>org.testng</groupId> <artifactId>testng</artifactId> <version>6.14.3</version> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <version>4.0.0</version> </dependency> </dependencies> |
Testing Response Body
In this test, I want to verify the entire body of the response. I can do that by calling Approvals.verify and passing in the body. While this verifies the body, the status code could still be wrong. So, I add one more call to Rest-Assured’s statusCode method to verify that as well.
1 2 3 4 5 6 |
@Test public void testWhiteHouseZipBody(){ Response response = given().get("http://api.zippopotam.us/us/20500"); Approvals.verify(response.body().prettyPrint()); response.then().statusCode(200); } |
When executed the first time, this will fail because there’s no approved file saved yet.
I take the result, make sure that it’s ok, and then save this into the approved file. This becomes my golden master.
When I run this again, Approval Test compares the received file (which now contains the new info) with the approved file. Since they both match, the test passes.
Testing Entire Response
While testing the response body along with the status code may be enough, it doesn’t hurt to go ahead and test the entire response – including the headers. I’ve certainly found bugs here before, such as with headers that contain pagination and such.
I created a new test to verify the entire response. Since the response as a whole also includes the status code, I no longer need a separate assertion just for that.
1 2 3 4 5 6 |
@Test public void testWhiteHouseZipAll(){ Response response = given().get("http://api.zippopotam.us/us/20500"); String responseStr = response.headers().toString() + "\n\n" + response.body().prettyPrint(); Approvals.verify(responseStr); } |
Dynamic Data
You’ll quickly run into an issue with using this technique: most responses are not static. For POST calls, the response may include a newly generated ID. Headers may include the date or cookie information that changes every time the test is run. Fortunately, I remembered Mark teaching us a little trick to deal with this.
Since we’re working with Strings here, we can simply do a replaceAll and use a regular expression to find the lines that we know are dynamic and then mask those lines.
In my response, there were three dynamic headers, so I masked them as follows:
1 2 3 4 5 6 7 8 9 10 11 |
@Test public void testWhiteHouseZipAll(){ Response response = given().get("http://api.zippopotam.us/us/20500"); String responseStr = response.headers().toString() + "\n\n" + response.body().prettyPrint(); responseStr = responseStr.replaceAll("Date=.*", "#####DATE"); responseStr = responseStr.replaceAll("Set-Cookie=.*", "#####COOKIE"); responseStr = responseStr.replaceAll("CF-RAY=.*", "#####CF-RAY"); Approvals.verify(responseStr); } |
The file contents are then saved as:
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 |
#####DATE Content-Type=application/json Transfer-Encoding=chunked Connection=keep-alive #####COOKIE X-Cache=hit Charset=UTF-8 Vary=Accept-Encoding Access-Control-Allow-Origin=* Server=cloudflare #####CF-RAY Content-Encoding=gzip { "post code": "20500", "country": "United States", "country abbreviation": "US", "places": [ { "place name": "Washington", "longitude": "-77.0355", "state": "District of Columbia", "state abbreviation": "DC", "latitude": "38.8946" } ] } |
Download Code
I’ve cleaned this up a bit and placed it on Github for your convenience.
Get Demo Code
David Garratt
Really cool
Nibs
Thanks Angie, But for an API responses with lot of dynamic data, seems to be bit difficult. JSON schema validations should be right fit there.
Stéphane Colson
Yes, great post (again) Angie. I agree with Nibs here.
Angie Jones
yep I agree
Shawn Bacot
This is a really interesting approach. I’m interested to hear what you think about using a schema validator like AJV for Javascript to validate responses. It allows you to check for the present of required properties, value types in each response and provide expected data values to validate against as well. I’ve been doing that coupled with focused assertions for expected response values, particularly when creating and updating objects with some success.
Pingback: Five Blogs – 26 June 2019 – 5blogs
Pingback: Make way for collaboration – What I Read Last Fortnight (30th June 2019) | Louise Gibbs – Software Tester
Lakshmikanthan
This is really interesting.
Will O.
This is great. I’ve tried it out and it works as expected. However, I need some help figuring out how I can move the approved.txt file(s) to a separate location for better structure without breaking the link to the test 🙂
Angie Jones
I actually contributed to a PR to do just that!
https://github.com/approvals/ApprovalTests.Java/blob/master/approvaltests/docs/Configuration.md#alternative-base-directory-for-output-files
Vindhiyan
Sam Spokowski
It seems like a lot of the features you are describing are all bundled in Jest’s snapshot testing functionality. Jest is a unit testing framework for JS projects. You can use it for testing APIs by pairing it with an HTTP call framework such as Axios. Jest has built in type matchers and can handle regex for verifying fields that you would expect to change each time. It also tells you the differences between your expected results and your actual results without having to go to a different tool (like DiffMerge). I would check it out https://jestjs.io/
Angie Jones
limited to JavaScript, right? I use Java.
Vladimir Belorusets
In my article “REST API Test Automation in Java with Open Source Tools” (2015) (http://www.methodsandtools.com/archive/restapitesting.php), I listed four tools for comparison of JSON actual and expected responses.
Vladimir Belorusets
For Windows 10 and Eclipse, DiffMerge does not pop up. You need to use TortoiseDiff.
Nagesh Kumar
A couple of issues here1. Whenever there is value mismatch in new response vs stored one, error thrown is “java.lang.Error: Failed Approval”. It doesn’t tell us exactly which field it is where we have a mismatch.2. The location where the approval files are getting stored makes the entire framework or codebase look messy. It can be moved to the resource folder or something OR we can use a annotation and save it in the respective folder. 3. API response keeps on changes frequently in APIs such as search, order details with many people in the org using the same database. So every time generating approved.txt is a pain
Angie Jones
1. When the test fails, a diff editor pops up to show the difference. I’m sure this can be automated for CI reporting
2. You can specify the directory where the files are output
https://twitter.com/techgirl1908/status/1143401266707517441
3. This seems like a test design issue. I wouldn’t automate tests for an API response that changes frequently. Sounds odd that a consistent request wouldn’t get a consistent response.
Like any tool, this one has its purposes and shouldn’t be used for all situations. If it doesn’t fit your model, don’t use it.
swapnil Bodade
Nice. looking forward more articles on API
Gaurav Khurana
Very good approach for static APIs matching.
if they can provide a way to add the dynamic field with some easy add function where we pass a list of attributes to ignore it would have been better
good for learning. thanks for sharing