TestingAPIs is still being built. If something breaks, email support@testingapis.com.

Sending bodies and chaining requests

Strings, maps and Java records as JSON, and a create, read, delete chain.

Reading data is half of an API. The other half is sending it: creating records, updating them, submitting forms. This lesson covers the ways REST Assured builds a request body, how to pull values like a new record's ID out of a response, and a test that creates a record, checks it and deletes it again. All of it is in RequestBodyTest.java:

RequestBodyTest.java
package com.testingapis.examples;

import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import io.restassured.http.ContentType;
import org.junit.jupiter.api.Test;

import java.util.Map;

import static com.testingapis.examples.ApiSpecs.api;
import static io.restassured.RestAssured.given;
import static org.hamcrest.MatcherAssert.assertThat;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.startsWith;

/** Lesson 4: three ways to send a body, and a create, read, delete chain that cleans up after itself. */
class RequestBodyTest {

    /** What we send to create a todo. Jackson turns it into JSON. */
    record NewTodo(String title, int userId, String priority) {
    }

    /** What the API sends back. Unknown fields are ignored, so a new field on the server doesn't break the test. */
    @JsonIgnoreProperties(ignoreUnknown = true)
    record Todo(int id, int userId, String title, boolean completed, String priority) {
    }

    @Test
    void aBodyAsAStringAMapOrAnObject() {
        String asString = "{\"name\":\"Asha\",\"role\":\"tester\"}";
        Map<String, Object> asMap = Map.of("name", "Asha", "role", "tester");

        for (Object body : new Object[] {asString, asMap}) {
            given()
                .spec(api())
                .contentType(ContentType.JSON)
                .body(body)
            .when()
                .post("/api/methods/post")
            .then()
                .statusCode(200)
                .body("contentType", startsWith("application/json"));
        }
    }

    @Test
    void createReadAndDeleteATodo() {
        String title = "REST Assured tutorial " + System.currentTimeMillis();

        int id = given()
            .spec(api())
            .contentType(ContentType.JSON)
            .body(new NewTodo(title, 1, "low"))
        .when()
            .post("/api/Todos")
        .then()
            .statusCode(201)
            .body("data.title", equalTo(title))
            .extract().path("data.id");

        try {
            Todo stored = given()
                .spec(api())
                .pathParam("id", id)
            .when()
                .get("/api/Todos/{id}")
            .then()
                .statusCode(200)
                .extract().as(Todo.class);

            assertThat(stored.title(), equalTo(title));
            assertThat(stored.priority(), equalTo("low"));
            assertThat(stored.completed(), equalTo(false));
        } finally {
            given().spec(api()).pathParam("id", id)
                .when().delete("/api/Todos/{id}")
                .then().statusCode(200);
        }

        given().spec(api()).pathParam("id", id)
            .when().get("/api/Todos/{id}")
            .then().statusCode(404);
    }
}

Three ways to write a body

BodyGood forWatch out for
A StringExact JSON, including deliberately broken JSON for negative testsEscaping quotes by hand; easy to get wrong
A MapQuick bodies in a test, without defining a typeTypos in field names aren't caught by the compiler
A Java object (here a record)Bodies used in many tests, and anything you also read backNeeds Jackson on the classpath, which the project has

The first test sends the same body as a string and as a map to an endpoint that echoes back what it received. For both, the echo shows the server got application/json:

What /api/methods/post saw
{
  "method": "POST",
  "message": "POST request received",
  "path": "/api/methods/post",
  "contentType": "application/json",
  "bodyLength": 31,
  "body": "{\"name\":\"Asha\",\"role\":\"tester\"}",
  ...
}

contentType(ContentType.JSON)sets that header, and it isn't optional. Without it, all three kinds of body, the string, the map and even the record, went out as text/plain; charset=ISO-8859-1: the echo endpoint reported exactly that for each. A JSON API will reject such a request, or worse, accept it and misread it. Set the content type on every request that has a body. Note too that REST Assured 6 sends application/json with no charset parameter, which is why the test checks that the value starts with application/json rather than comparing it exactly.

Records as bodies

NewTodo is a Java record with three fields. Passed to .body(), it becomes {"title": "...", "userId": 1, "priority": "low"}, with field names taken from the record's components. Typos in field names become compile errors instead of mysterious 400 responses, and the same record can be reused by every test that creates a todo.

Create, read, delete

The second test follows one todo through its life. The create response looks like this:

POST /api/Todos (201 Created)
{
  "message": "Todo created successfully",
  "data": {
    "id": 31,
    "userId": 1,
    "title": "REST Assured tutorial 1791727763082",
    "completed": false,
    "priority": "low",
    "dueDate": ""
  }
}

That test uses three techniques. Here they are one at a time:

  • .extract().path("data.id") pulls the new ID out of the response, after the checks in then() have passed. The variable's type, int, tells REST Assured what to return.
  • .pathParam("id", id) with "/api/Todos/{id}"fills in the URL. It's safer than building the URL with string concatenation, because REST Assured encodes the value.
  • .extract().as(Todo.class) turns the response into a Todo record, so the checks that follow are plain Java on typed fields. @JsonIgnoreProperties(ignoreUnknown = true)means a field added to the API later won't break the test.

The title includes the current time, so it's unique to the run, and the read-back compares it exactly. On a shared API, a fixed title like "test" could match someone else's todo. The REST tutorial's lesson on testing a resource end to end explains why the read-back matters even when the create response looks right.

Exercise

Add a test that creates a todo, changes its title with PUT /api/Todos/{id} using a second record, reads it back and checks the new title, then deletes it. Then add a negative test: send the string "{\"title\": ", which is broken JSON, to POST /api/Todos and check the status the API returns for it.

Practise and go further