Checking JSON with GPath and Hamcrest
Nested fields, lists, find and findAll, the Float trap, and extracting values.
Most of a REST Assured test is checks on the JSON body. The first argument to .body()is a path into the response, written in GPath, a small expression language from Groovy that REST Assured uses underneath. The second is a Hamcrest matcher. This lesson covers the paths and matchers you'll use most, including one numeric trap that catches nearly everyone, using JsonAssertionsTest.java from the example project.
Nested fields
Product 1 looks like this:
{
"id": 1,
"title": "Wireless Noise-Cancelling Headphones",
"price": 249.99,
"category": "electronics",
"rating": { "rate": 4.3, "count": 127 },
"inStock": true,
...
}Dots walk into objects, so the rating's fields are rating.rate and rating.count:
@Test
void nestedFieldsAndNumbers() {
given()
.spec(api())
.when()
.get("/api/Products/1")
.then()
.statusCode(200)
.body("rating.rate", equalTo(4.3f)) // JSON decimals arrive as Float by default
.body("rating.count", equalTo(127))
.body("price", equalTo(249.99f));
}The Float trap
Look at the decimals in that test: 4.3f and 249.99f, with an f. REST Assured reads JSON numbers with a decimal point as Java Float. Write the obvious equalTo(249.99), which is a double, and the test fails even though the numbers look identical:
.body("price", equalTo(249.99))
1 expectation failed.
JSON path price doesn't match.
Expected: <249.99>
Actual: <249.99F>249.99 and 249.99F are different types, so equalTo says no. Integers like 127 come back as Integer and compare fine. For prices and other money, where float rounding is a real risk, you can ask for BigDecimal instead:
given()
.baseUri("https://api.testingapis.com")
.config(config().jsonConfig(jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.BIG_DECIMAL)))
.when()
.get("/api/Products/1")
.then()
.body("price", equalTo(new BigDecimal("249.99")));That passes. It needs imports for JsonPathConfig, BigDecimal, and the static config and jsonConfig. Lesson 6 shows how to set it once for every test rather than in each one.
Lists
When a path lands on a list, REST Assured collects the values for you. On a list of products, "category" means every product's category, and "size()" means how many there are. GPath also has find, which returns the first match, and findAll, which returns every match:
@Test
void everyItemInAFilteredList() {
given()
.spec(api())
.queryParam("category", "electronics")
.when()
.get("/api/Products")
.then()
.statusCode(200)
.header("X-Total-Count", notNullValue())
.body("size()", greaterThanOrEqualTo(5))
.body("category", everyItem(equalTo("electronics")))
.body("id", hasItems(1, 2, 3))
.body("find { it.id == 3 }.title", equalTo("Mechanical Keyboard"))
.body("findAll { it.price < 100 }.title", hasItem("Mechanical Keyboard"));
}| Path | Means |
|---|---|
category | The category of every item, as a list |
size() | The number of items |
find { it.id == 3 }.title | The title of the first item whose id is 3 |
findAll { it.price < 100 }.title | The titles of every item cheaper than 100 |
[0].title | The title of the first item |
The matchers pair with those paths:
everyItem(equalTo("electronics")): every value in the list matches, which is the right check for a filter.hasItems(1, 2, 3): the list contains these, in any order, possibly among others.hasItem("Mechanical Keyboard"): at least one value matches.greaterThanOrEqualTo(5): a lower bound, rather than an exact count.
Notice there's no exact count. This test API is shared, and anyone can add a product, so the number of electronics products changes. Asserting at least the five from the seed data, and that every item really is electronics, tests what the API promises without failing whenever someone adds a product. The REST tutorial's lesson on filtering, sorting and pagination covers what else to check on list endpoints.
Extracting values for plain Java checks
Some checks are easier in Java than in a matcher. extract().jsonPath() gives you the parsed response to query as many times as you like:
@Test
void extractValuesForPlainJavaChecks() {
JsonPath json = given()
.spec(api())
.queryParam("category", "electronics")
.when()
.get("/api/Products")
.then()
.statusCode(200)
.extract().jsonPath();
List<Float> prices = json.getList("price", Float.class);
float cheapest = json.getFloat("min { it.price }.price");
assertThat(prices, everyItem(greaterThanOrEqualTo(cheapest)));
assertThat(json.getString("[0].title"), notNullValue());
}getList("price", Float.class) returns the prices as a typed list, and min { it.price }finds the cheapest item. From there it's ordinary Java, with Hamcrest's assertThat for the checks. Use extraction when you need a value later in the test, or a check that needs a loop or arithmetic. For everything else, checks inside then() read better and report better.
Exercise
Write a test for GET /api/Todos?completed=true that checks every item has completed set to true, that the list contains at least one todo with priority high, and that find { it.id == 3 }.title is not empty. Then extract all the titles with getList and assert that none of them is blank, using plain Java.