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

Request and response specifications

One place for the base URL, keys and logging, and checks every response must pass.

By the third test class the same lines keep appearing: the base URL, the Acceptheader, a key. Copy them into every test and changing the server means editing every file. REST Assured's answer is specifications: a request specification holds setup shared by many requests, and a response specification holds checks shared by many responses. Each test then starts from them and adds only what's particular to it.

A shared request specification

The example project keeps its shared setup in one class:

ApiSpecs.java
package com.testingapis.examples;

import io.restassured.RestAssured;
import io.restassured.builder.RequestSpecBuilder;
import io.restassured.builder.ResponseSpecBuilder;
import io.restassured.filter.log.LogDetail;
import io.restassured.http.ContentType;
import io.restassured.specification.RequestSpecification;
import io.restassured.specification.ResponseSpecification;

import static org.hamcrest.Matchers.lessThan;

/** Shared setup for every test: where the API is, the key if there is one, and what every response must satisfy. */
public final class ApiSpecs {

    /** -DbaseUrl=... on the command line, or BASE_URL in the environment, or the public test API. */
    public static final String BASE_URL = System.getProperty("baseUrl",
            System.getenv().getOrDefault("BASE_URL", "https://api.testingapis.com"));

    static {
        // Print the full request and response only when a test fails, so passing runs stay quiet
        RestAssured.enableLoggingOfRequestAndResponseIfValidationFails(LogDetail.ALL);
    }

    private ApiSpecs() {
    }

    /** The request setup every test starts from. */
    public static RequestSpecification api() {
        RequestSpecBuilder builder = new RequestSpecBuilder()
                .setBaseUri(BASE_URL)
                .setAccept(ContentType.JSON);

        // Endpoints outside the free tier need a testingapis.com key; send it only when one is configured
        String key = System.getenv("TESTINGAPIS_KEY");
        if (key != null && !key.isBlank()) {
            builder.addHeader("X-TestingApis-Key", key);
        }
        return builder.build();
    }

    /** Checks that apply to every response, whatever the endpoint. */
    public static ResponseSpecification fastResponse() {
        return new ResponseSpecBuilder()
                .expectResponseTime(lessThan(5000L))
                .build();
    }
}

Four decisions are made in one place:

WhatHowWhy
Which server-DbaseUrl=, then BASE_URL, then the public APIThe same tests run against a laptop, staging or production without code changes
The API keyRead from TESTINGAPIS_KEY, sent only if setSecrets stay out of the code and out of version control
The Accept headersetAccept(ContentType.JSON)Every request asks for JSON
LoggingenableLoggingOfRequestAndResponseIfValidationFailsPassing runs stay quiet; failures show the whole exchange

A test adds the spec with .spec(api()) and builds on top of it:

java
given()
    .spec(api())              // base URL, Accept header, the API key if one is set
    .queryParam("completed", true)
.when()
    .get("/api/Todos")
.then()
    .spec(fastResponse())     // every response within 5 seconds
    .statusCode(200)

Anything set in the test, like that query parameter, is merged with the spec. The same mechanism works for path parameters, extra headers and bodies.

Running against another server

The whole suite, pointed at a local copy of the API instead of the public one:

Terminal
$ mvn test -DbaseUrl=http://localhost:5299
[INFO] Tests run: 23, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS

One property, no edits. That's also how the same suite runs in CI against a test environment, in lesson 8. One exception in the project: FirstTest still writes the base URL out in full, because it comes before this lesson. Tests that skip the shared spec are exactly the ones that end up pointing at the wrong server.

A shared response specification

fastResponse() holds checks that apply whatever the endpoint, here that every response arrives within five seconds. A test applies it in then() with .spec(fastResponse()):

SpecsTest.java
package com.testingapis.examples;

import org.junit.jupiter.api.Tag;
import org.junit.jupiter.api.Test;

import static com.testingapis.examples.ApiSpecs.api;
import static com.testingapis.examples.ApiSpecs.fastResponse;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;

/** Lesson 6: shared request and response specifications instead of repeated setup. */
class SpecsTest {

    @Test
    @Tag("smoke")
    void healthCheck() {
        given()
            .spec(api())
        .when()
            .get("/api/health")
        .then()
            .spec(fastResponse())
            .statusCode(200)
            .body("status", equalTo("healthy"));
    }

    @Test
    void completedTodosAreAllCompleted() {
        given()
            .spec(api())
            .queryParam("completed", true)
        .when()
            .get("/api/Todos")
        .then()
            .spec(fastResponse())
            .statusCode(200)
            .body("completed", everyItem(equalTo(true)));
    }
}

Keep response specs to things that really are universal. A spec that also checks statusCode(200)can't be used by negative tests, and teams end up with five nearly identical specs. Time limits, required headers and content type are good candidates. Keep the time limit generous: five seconds catches a request that has really hung, while a tight limit fails whenever a CI machine is busy.

Logging only when a test fails

REST Assured can log every request and response, which drowns a big suite in output. The setting in the static block of ApiSpecs logs only when a check fails. Here is what a failed health check prints:

Terminal
Request method:	GET
Request URI:	https://api.testingapis.com/api/health
Headers:		Accept=application/json, application/javascript, text/javascript, text/json
Body:			<none>

HTTP/1.1 200 OK
content-type: application/json; charset=utf-8
...
{
    "status": "healthy",
    ...
}
java.lang.AssertionError:
1 expectation failed.
JSON path status doesn't match.
Expected: up
  Actual: healthy

The request, the response and the failed check together, which is usually everything needed to tell a test bug from an API bug. For a single test you can still log everything with given().log().all() and then().log().all() while you work on it.

Settings for every request

Specs can carry REST Assured configuration too. Lesson 3 showed how to read decimals as BigDecimal in one test; in the shared spec, it applies to every test that uses it:

java
new RequestSpecBuilder()
    .setBaseUri(BASE_URL)
    .setConfig(RestAssuredConfig.config()
        .jsonConfig(jsonConfig().numberReturnType(JsonPathConfig.NumberReturnType.BIG_DECIMAL)))
    .build();

Checked against product 1, .body("price", equalTo(new BigDecimal("249.99"))) passes with this spec. The trade-off is that every decimal check in the suite must then use BigDecimal, so decide early.

  • There's also a global way: setting RestAssured.baseURIand friends in static fields. It's shorter, and it's shared mutable state, so one test that changes it affects every test after it. Specs are explicit and safer.
  • For several APIs, write one spec method per API rather than one spec that tries to cover all of them.

Exercise

Add a second response spec, jsonResponse(), that expects the content type to be JSON, and use it alongside fastResponse() in SpecsTest. Then change FirstTest to use api() instead of its own base URL, and run the whole suite with -DbaseUrl=https://api.testingapis.com/nope. How many tests fail, and how quickly can you tell why from the logs?

Practise and go further