Software Testing & Automation · Lesson 8 of 11 · Updated ·
API Testing with REST Assured — GET, POST, Auth Tokens, Status Codes and JSON Checks
UI tests check what users see; API tests check the business rules underneath — faster, more stable, and possible before the UI even exists. REST Assured is the standard Java library for them.
Our API tests run against the Course API we build in the Spring Boot track: courses, login with JWT tokens, roles.
1. HTTP in two minutes
A request has a method (GET read, POST create, PUT update, DELETE remove), a URL, headers (like Content-Type: application/json, Authorization: Bearer …) and maybe a JSON body. The response has a status code (200 OK, 201 Created, 400 bad input, 401 not logged in, 403 not allowed, 404 not found, 409 conflict, 500 server error), headers and a body.
2. The tests
// File: src/test/java/com/skills101/qa/api/CourseApiTest.java
package com.skills101.qa.api;
import static io.restassured.RestAssured.given;
import static org.hamcrest.Matchers.equalTo;
import static org.hamcrest.Matchers.everyItem;
import static org.hamcrest.Matchers.greaterThan;
import static org.hamcrest.Matchers.hasItems;
import static org.hamcrest.Matchers.notNullValue;
import io.restassured.RestAssured;
import io.restassured.http.ContentType;
import org.testng.annotations.BeforeClass;
import org.testng.annotations.Test;
/**
* API tests against the running Course API (examples/spring-boot/course-api).
* Start it first, or point the tests elsewhere with -DapiUrl=https://staging.example.com
*/
public class CourseApiTest {
@BeforeClass(alwaysRun = true)
public void setUp() {
RestAssured.baseURI = System.getProperty("apiUrl", "http://localhost:8081");
RestAssured.enableLoggingOfRequestAndResponseIfValidationFails();
}
private String tokenFor(String email, String password) {
return given()
.contentType(ContentType.JSON)
.body("{\"email\":\"" + email + "\",\"password\":\"" + password + "\"}")
.when()
.post("/auth/token")
.then()
.statusCode(200)
.body("type", equalTo("Bearer"))
.extract().path("token");
}
@Test(groups = "smoke")
public void listCourses() {
given()
.when()
.get("/api/courses")
.then()
.statusCode(200)
.contentType(ContentType.JSON)
.body("title", hasItems("Java Full Stack", "DSA in Python"))
.body("fee", everyItem(greaterThan(0)));
}
@Test
public void getOneCourse() {
given()
.pathParam("id", 1)
.when()
.get("/api/courses/{id}")
.then()
.statusCode(200)
.body("title", equalTo("Java Full Stack"))
.body("fee", equalTo(4999));
}
@Test
public void unknownCourseIs404WithProblemDetail() {
given()
.when()
.get("/api/courses/9999")
.then()
.statusCode(404)
.contentType("application/problem+json")
.body("detail", equalTo("Course 9999 not found"));
}
@Test
public void createWithoutTokenIs401() {
given()
.contentType(ContentType.JSON)
.body("{\"title\":\"Selenium Basics\",\"fee\":999,\"seats\":20}")
.when()
.post("/api/courses")
.then()
.statusCode(401);
}
@Test
public void adminCreatesCourseAndCanReadItBack() {
String token = tokenFor("admin@skills101.in", "admin123");
int id = given()
.auth().oauth2(token) // adds "Authorization: Bearer <token>"
.contentType(ContentType.JSON)
.body("{\"title\":\"API Testing with REST Assured\",\"fee\":1499,\"seats\":25}")
.when()
.post("/api/courses")
.then()
.statusCode(201)
.header("Location", notNullValue())
.body("title", equalTo("API Testing with REST Assured"))
.extract().path("id");
given().pathParam("id", id)
.when().get("/api/courses/{id}")
.then().statusCode(200).body("seats", equalTo(25));
}
@Test
public void invalidCourseIs400WithFieldErrors() {
String token = tokenFor("admin@skills101.in", "admin123");
given()
.auth().oauth2(token)
.contentType(ContentType.JSON)
.body("{\"title\":\"\",\"fee\":-1,\"seats\":0}")
.when()
.post("/api/courses")
.then()
.statusCode(400)
.body("errors.title", equalTo("must not be blank"))
.body("errors.seats", equalTo("must be greater than or equal to 1"));
}
@Test
public void studentCannotCreateCourses() {
String token = tokenFor("asha@skills101.in", "asha123");
given()
.auth().oauth2(token)
.contentType(ContentType.JSON)
.body("{\"title\":\"Nope\",\"fee\":1,\"seats\":1}")
.when()
.post("/api/courses")
.then()
.statusCode(403);
}
}
All 7 tests pass against the running API. What they show:
- given / when / then reads like a sentence: given these headers and body, when I POST, then expect 201.
- JSON paths:
body("title", equalTo(...))reads a field; on a list,"title"gives all titles, sohasItems(...)andeveryItem(...)check collections. Nested fields:"errors.title". - Auth:
tokenFor()logs in,extract().path("token")pulls the token out, and.auth().oauth2(token)sends it as a Bearer header. - Chaining: create a course, extract its
id, then GET it back. - Negative tests matter as much as happy paths: 401 without a token, 403 for a student, 400 for invalid data, 404 for unknown ids.
enableLoggingOfRequestAndResponseIfValidationFails()prints the full request and response only when a test fails — great for debugging.- The base URL comes from
-DapiUrl, so the same tests run against local, staging or production-like environments.
3. More you'll use at work
- JSON schema validation (
matchesJsonSchemaInClasspath("course-schema.json")) — checks the shape of every field at once. - POJOs instead of JSON strings:
.body(new CourseRequest("Git", 0, 50)), and.extract().as(CourseResponse.class). - Request/response specifications to share base URI, headers and auth across tests.
- Response time:
.time(lessThan(2000L)).
Common mistakes
- Only testing happy paths.
- Hard-coding tokens that expire — log in during the test.
- Asserting the whole JSON as a string (breaks when field order changes).
- Tests that depend on data created by other tests; create what you need, ideally with unique names.
Quick recap
- given (headers, auth, body) → when (method + path) → then (status, headers, body).
- JSON paths + Hamcrest matchers check fields and lists.
- Log in during the test, extract the token, send it as Bearer.
- Cover negative cases: 400, 401, 403, 404.