Serenity BDD REST API Testing: From Basics to Reports
Serenity BDD includes built-in REST API testing support via the serenity-rest-assured module. It wraps REST Assured with Serenity's reporting engine, so API tests produce the same structured HTML reports as browser tests — step details, request/response logs, and requirements traceability.
Setup
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-rest-assured</artifactId>
<version>4.1.20</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>4.1.20</version>
<scope>test</scope>
</dependency>Basic GET Request
import net.serenitybdd.rest.SerenityRest;
import net.thucydides.core.annotations.Step;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import static org.hamcrest.Matchers.*;
@ExtendWith(SerenityJUnit5Extension.class)
class UserApiTest {
@Test
void get_all_users_returns_list() {
SerenityRest.given()
.baseUri("https://api.example.com")
.header("Accept", "application/json")
.when()
.get("/users")
.then()
.statusCode(200)
.body("users", hasSize(greaterThan(0)))
.body("users[0].id", notNullValue())
.body("users[0].email", containsString("@"));
}
@Test
void get_user_by_id_returns_correct_user() {
SerenityRest.given()
.baseUri("https://api.example.com")
.pathParam("id", 42)
.when()
.get("/users/{id}")
.then()
.statusCode(200)
.body("id", equalTo(42))
.body("email", equalTo("alice@example.com"));
}
@Test
void get_nonexistent_user_returns_404() {
SerenityRest.given()
.baseUri("https://api.example.com")
.when()
.get("/users/99999")
.then()
.statusCode(404)
.body("error", equalTo("User not found"));
}
}SerenityRest is a drop-in replacement for REST Assured's RestAssured. Every request is logged in the Serenity report with request details and response body.
POST, PUT, DELETE
@Test
void create_user_returns_201() {
String requestBody = """
{
"email": "newuser@example.com",
"name": "New User",
"role": "viewer"
}
""";
SerenityRest.given()
.baseUri("https://api.example.com")
.header("Content-Type", "application/json")
.body(requestBody)
.when()
.post("/users")
.then()
.statusCode(201)
.header("Location", containsString("/users/"))
.body("id", notNullValue())
.body("email", equalTo("newuser@example.com"));
}
@Test
void update_user_modifies_name() {
SerenityRest.given()
.baseUri("https://api.example.com")
.header("Content-Type", "application/json")
.pathParam("id", 42)
.body("{\"name\": \"Updated Name\"}")
.when()
.put("/users/{id}")
.then()
.statusCode(200)
.body("name", equalTo("Updated Name"));
}
@Test
void delete_user_returns_204() {
SerenityRest.given()
.baseUri("https://api.example.com")
.pathParam("id", 42)
.when()
.delete("/users/{id}")
.then()
.statusCode(204);
}Step Libraries for API Tests
Wrap API calls in @Step methods for cleaner test code and better report output:
public class UserApiSteps {
private static final String BASE_URL = "https://api.example.com";
@Step("Get all users")
public ValidatableResponse getAllUsers() {
return SerenityRest.given()
.baseUri(BASE_URL)
.header("Accept", "application/json")
.when()
.get("/users")
.then();
}
@Step("Get user with id {0}")
public ValidatableResponse getUserById(int id) {
return SerenityRest.given()
.baseUri(BASE_URL)
.pathParam("id", id)
.when()
.get("/users/{id}")
.then();
}
@Step("Create user with email {0}")
public ValidatableResponse createUser(String email, String name) {
return SerenityRest.given()
.baseUri(BASE_URL)
.header("Content-Type", "application/json")
.body(Map.of("email", email, "name", name))
.when()
.post("/users")
.then();
}
@Step("Delete user {0}")
public ValidatableResponse deleteUser(int id) {
return SerenityRest.given()
.baseUri(BASE_URL)
.pathParam("id", id)
.when()
.delete("/users/{id}")
.then();
}
}@ExtendWith(SerenityJUnit5Extension.class)
class UserApiTest {
@Steps
UserApiSteps api;
@Test
void create_and_retrieve_user() {
// Create
int userId = api.createUser("test@example.com", "Test User")
.statusCode(201)
.extract().path("id");
// Retrieve
api.getUserById(userId)
.statusCode(200)
.body("email", equalTo("test@example.com"));
// Cleanup
api.deleteUser(userId)
.statusCode(204);
}
}Authentication
Bearer Token
SerenityRest.given()
.baseUri("https://api.example.com")
.header("Authorization", "Bearer " + getAuthToken())
.when()
.get("/protected/resource")
.then()
.statusCode(200);Basic Auth
SerenityRest.given()
.baseUri("https://api.example.com")
.auth().basic("username", "password")
.when()
.get("/users")
.then()
.statusCode(200);OAuth2
SerenityRest.given()
.baseUri("https://api.example.com")
.auth().oauth2(accessToken)
.when()
.get("/users/me")
.then()
.statusCode(200);Combining API and Browser Tests
Serenity supports mixing API and browser tests in the same report. Use the API to set up test data, then browser tests to verify the UI:
@ExtendWith(SerenityJUnit5Extension.class)
class UserManagementTest {
@Steps
UserApiSteps api;
@Managed
WebDriver driver;
@Test
void new_user_appears_in_admin_ui() {
// Create user via API
int userId = api.createUser("newuser@example.com", "New User")
.statusCode(201)
.extract().path("id");
// Verify in browser
driver.get("https://example.com/admin/users");
assertThat(driver.findElement(By.xpath("//td[text()='newuser@example.com']"))
.isDisplayed()).isTrue();
// Cleanup
api.deleteUser(userId).statusCode(204);
}
}Both the API calls and browser interactions appear as steps in the same test report.
Response Extraction and Chaining
@Test
void create_order_and_verify_confirmation() {
// Create order
String orderId = SerenityRest.given()
.baseUri("https://api.example.com")
.header("Content-Type", "application/json")
.body(orderPayload)
.when()
.post("/orders")
.then()
.statusCode(201)
.extract().path("orderId");
// Verify confirmation email was queued
SerenityRest.given()
.baseUri("https://api.example.com")
.queryParam("orderId", orderId)
.when()
.get("/notifications/pending")
.then()
.statusCode(200)
.body("notifications.orderId", hasItem(orderId));
}Report Output
API test reports in Serenity show:
- Request method, URL, headers, and body
- Response status, headers, and body
- Step names from
@Stepannotations - Assertions and whether they passed
This means a test failure includes the exact request that was made and the response that came back — no need to re-run with extra logging to diagnose.
Summary
serenity-rest-assured brings Serenity's reporting to API tests. Every request is logged in the HTML report with full request/response details. Combined with @Step-annotated step libraries and @Epic/@Feature annotations, API tests slot into the same requirements hierarchy as browser tests.
The main benefit over plain REST Assured is the report. If your team already uses Serenity for browser tests, using SerenityRest instead of RestAssured is a low-effort way to bring API tests into the same reporting structure.