API testing · Java
API automation
Sending requests and checking responses in code (REST Assured or requests).
Learn: API automation in Java with REST Assured9 minute read
REST Assured is a Java library for testing HTTP APIs: it sends requests and checks responses in a few readable lines. Most Java teams use it, and interviewers often ask you to write a test live ("automate this POST endpoint") and then ask how you would keep a large suite (a big group of tests) clean. This article walks from the first request to those habits. It assumes you know basic Java and have seen JSON before (see the HTTP topic). All snippets use REST Assured 6 with Hamcrest matchers (small objects such as equalTo that describe an expected value) and assume these imports:
import static io.restassured.RestAssured.*; import static org.hamcrest.Matchers.*; import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; import io.restassured.response.Response; import io.restassured.specification.RequestSpecification; import io.restassured.specification.ResponseSpecification; import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
given, when, then
A REST Assured test reads like a sentence. given() builds the request, when() sends it, and then() checks the response:
given()
.baseUri("https://api.example.com")
.queryParam("active", true)
.when()
.get("/users/{id}", 42)
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("id", equalTo(42))
.body("address.city", equalTo("Pune"));
The path in body() is a GPath expression: REST Assured's way of pointing to one value inside the JSON, like address.city in the example above. A path through a list collects values from every element, so with a body like {"items": [{"name": "Asha"}, {"name": "Ravi"}]}:
.body("items", hasSize(2))
.body("items.name", hasItems("Asha", "Ravi"))
.body("items[0].name", equalTo("Asha"))
queryParam() and pathParam() encode values for you, so search text with spaces or an ampersand (the & character, which normally separates query parameters) is sent safely.
A POST that creates something
Map<String, Object> user = Map.of("name", "Asha", "email", "asha@example.com");
int id =
given()
.contentType(ContentType.JSON)
.body(user)
.when()
.post("/users")
.then()
.statusCode(201)
.header("Location", containsString("/users/"))
.body("name", equalTo("Asha"))
.extract().path("id");
given().when().get("/users/{id}", id)
.then().statusCode(200).body("email", equalTo("asha@example.com"));
REST Assured turns the map into JSON (a JSON library such as Jackson or Gson must be on the classpath, which means it must be added to the project as a dependency). Setting the content type matters: without it many servers answer 415. The final GET proves the user was really saved, which is what interviewers want to see after "write a test for this POST".
Extracting values and chaining calls
extract().path("token") returns one value. For several values, keep the Response:
Response login = given().contentType(ContentType.JSON).body(creds).post("/auth/login");
login.then().statusCode(200);
String token = login.jsonPath().getString("token");
given().auth().oauth2(token) // sends Authorization: Bearer <token>
.contentType(ContentType.JSON)
.body(Map.of("productId", 2, "quantity", 1))
.when().post("/orders")
.then().statusCode(201);
auth().oauth2(token) and header("Authorization", "Bearer " + token) send the same header. For Basic authentication, auth().basic(user, pass) waits for the server to answer 401 and ask for credentials (see the authentication topic) before sending them, while auth().preemptive().basic(user, pass) sends them on the first request, which many APIs require.
Specifications: write the setup once
RequestSpecification api = new RequestSpecBuilder()
.setBaseUri(System.getProperty("baseUrl", "https://staging.example.com"))
.setContentType(ContentType.JSON)
.addHeader("Authorization", "Bearer " + token)
.build();
ResponseSpecification created = new ResponseSpecBuilder()
.expectStatusCode(201)
.expectContentType(ContentType.JSON)
.build();
given().spec(api).body(order).when().post("/orders").then().spec(created);
A request specification holds the base URI, headers and authentication; a response specification holds checks that every response of a kind must pass. Build them once, for example in a @BeforeClass method (a TestNG method that runs once before the tests of a class) or a small client class, and each test states only what is special about it.
POJOs, schemas and logging
A POJO is a plain Java class with fields, used here to hold a request or response body. body(order) turns an object into JSON, and as(User.class) turns a response into one:
User created = given().spec(api).body(newUser).post("/users").as(User.class);
assertEquals("Asha", created.getName());
The compiler then catches typos in field names. One trap: with Jackson, a response field that the class does not declare makes as() fail by default, so annotate response classes with @JsonIgnoreProperties(ignoreUnknown = true) unless you want new fields to break the test.
import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath;
get("/users/7").then().body(matchesJsonSchemaInClasspath("schemas/user.json"));
The json-schema-validator module checks the whole response shape against a JSON schema, a file that lists the fields and types a response must have; value assertions still check the values for this case. log().ifValidationFails() on given() and then() prints the request and response only when a check fails, which keeps CI logs short. Hide (mask) tokens if logs are shared.
Other everyday checks
then().time(lessThan(2000L))fails slow responses. It is a sanity check, not a load test.multiPart("file", new File("invoice.pdf"))uploads a file asmultipart/form-data, the format browsers use for uploads.- Cookies come from
response.getCookie("name")and go back withcookie("name", value), the way a session-based API expects. - For XML responses the same
body()paths work, andxmlPath()extracts values.
How to answer in an interview
When asked to write a test live, say each step as you type: content type, body, the expected status, the key fields, then a GET to prove the data was saved. For "How is your suite organized?", name specifications for shared setup, POJOs for bodies, a @DataProvider (a TestNG feature that runs one test once for each row of data) for validation rules with many inputs, credentials from system properties or environment variables, and negative tests next to the positive ones.
Common mistakes
- Forgetting
contentType(ContentType.JSON)on a POST and getting 415. - Asserting only the status code.
- Comparing a list path such as
items.namewithequalToon one string. - Repeating the base URI and token in every test instead of a specification.
Recap
given() builds, when() sends, then() checks with GPath paths and Hamcrest matchers. Extract values to chain calls, keep shared setup in specifications, use POJOs for larger bodies and a schema for the overall shape, and log only on failure. Give each test its own data so the suite can run in parallel.
20 questions0/20 solved
The first 2 questions of each topic are free. Pro+ opens every question.