Serenity BDD + Cucumber + REST Assured: API and UI Testing Together
Serenity BDD integrates with REST Assured to give you the same rich HTML reports for API tests that you get for UI tests. Combine this with Cucumber and you can write acceptance tests that mix API calls and browser interactions in a single scenario — useful for tests that need to set up state via API before interacting with the UI, or verify backend state after a UI action.
Why Combine These Three
- Cucumber provides Gherkin scenarios that business and QA can read
- REST Assured handles HTTP requests with a fluent Java DSL
- Serenity wraps both in step-level reporting with full request/response logging in the HTML report
Without Serenity, REST Assured tests pass or fail silently. With Serenity, every API call appears in the report with the request, response, status code, and timing — the same narrative structure as UI tests.
Project Setup
<dependencies>
<!-- Serenity core + Cucumber -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-core</artifactId>
<version>3.9.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-cucumber</artifactId>
<version>3.9.8</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-junit5</artifactId>
<version>3.9.8</version>
<scope>test</scope>
</dependency>
<!-- REST Assured + Serenity integration -->
<dependency>
<groupId>net.serenity-bdd</groupId>
<artifactId>serenity-rest-assured</artifactId>
<version>3.9.8</version>
<scope>test</scope>
</dependency>
<!-- JSON assertions -->
<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest</artifactId>
<version>2.2</version>
<scope>test</scope>
</dependency>
</dependencies>The serenity-rest-assured module wraps REST Assured's RestAssured static class with SerenityRest — a drop-in replacement that logs every request and response into the Serenity report.
Writing API Feature Files
# src/test/resources/features/api/product_catalog.feature
Feature: Product Catalog API
Background:
Given the API base URL is configured
Scenario: Retrieve product by ID
When I request product with ID 42
Then the response status code should be 200
And the product name should be "Wireless Headphones"
And the product price should be 89.99
Scenario: Create a new product
Given I have a product with name "Mechanical Keyboard" and price 149.99
When I submit the create product request
Then the response status code should be 201
And the response should include a product ID
And the product should be retrievable via its ID
Scenario Outline: Product search returns correct results
When I search for products with query "<query>"
Then the response should contain <count> results
And all results should have category "<category>"
Examples:
| query | count | category |
| headphones | 3 | Audio |
| keyboard | 5 | Peripherals |
| monitor | 4 | Displays |API Step Definitions with SerenityRest
package com.example.steps.api;
import io.cucumber.java.en.*;
import io.restassured.response.Response;
import net.serenitybdd.rest.SerenityRest;
import net.thucydides.core.annotations.Step;
import org.assertj.core.api.Assertions;
import static net.serenitybdd.rest.SerenityRest.restAssuredThat;
public class ProductApiSteps {
private static final String BASE_URL = System.getProperty(
"api.base.url", "https://api.staging.example.com"
);
private Response lastResponse;
private String createdProductId;
@Given("the API base URL is configured")
public void configureBaseUrl() {
SerenityRest.setDefaultBasePath(BASE_URL);
}
@When("I request product with ID {int}")
public void getProductById(int id) {
lastResponse = SerenityRest
.given()
.header("Accept", "application/json")
.when()
.get("/products/{id}", id)
.then()
.extract().response();
}
@Then("the response status code should be {int}")
public void verifyStatusCode(int expectedCode) {
restAssuredThat(response -> response.statusCode(expectedCode));
}
@Then("the product name should be {string}")
public void verifyProductName(String expectedName) {
restAssuredThat(response ->
response.body("name", org.hamcrest.Matchers.equalTo(expectedName))
);
}
@Then("the product price should be {double}")
public void verifyProductPrice(double expectedPrice) {
restAssuredThat(response ->
response.body("price", org.hamcrest.Matchers.equalTo((float) expectedPrice))
);
}
@Given("I have a product with name {string} and price {double}")
public void prepareProductPayload(String name, double price) {
// Store for next step
this.productName = name;
this.productPrice = price;
}
@When("I submit the create product request")
public void createProduct() {
String payload = String.format(
"{\"name\": \"%s\", \"price\": %.2f}", productName, productPrice
);
lastResponse = SerenityRest
.given()
.header("Content-Type", "application/json")
.header("Authorization", "Bearer " + getTestToken())
.body(payload)
.when()
.post("/products")
.then()
.extract().response();
createdProductId = lastResponse.jsonPath().getString("id");
}
@Then("the response should include a product ID")
public void verifyProductIdPresent() {
Assertions.assertThat(createdProductId).isNotNull().isNotEmpty();
}
@Then("the product should be retrievable via its ID")
public void verifyProductRetrievable() {
SerenityRest
.given()
.header("Accept", "application/json")
.when()
.get("/products/{id}", createdProductId)
.then()
.statusCode(200);
}
private String productName;
private double productPrice;
private String getTestToken() {
return System.getProperty("api.test.token", "test-token-123");
}
}Reusable API Step Libraries
For steps shared across multiple features, create step library classes:
package com.example.steps.api;
import net.serenitybdd.rest.SerenityRest;
import net.thucydides.core.annotations.Step;
import io.restassured.response.Response;
public class AuthApiSteps {
@Step("Authenticate as user {0} and return token")
public String authenticateAndGetToken(String email, String password) {
return SerenityRest
.given()
.header("Content-Type", "application/json")
.body(String.format(
"{\"email\":\"%s\",\"password\":\"%s\"}", email, password
))
.when()
.post("/auth/login")
.then()
.statusCode(200)
.extract()
.jsonPath()
.getString("token");
}
@Step("Invalidate token for user {0}")
public void logout(String token) {
SerenityRest
.given()
.header("Authorization", "Bearer " + token)
.when()
.post("/auth/logout")
.then()
.statusCode(204);
}
}Mixed UI and API Scenarios
The real power comes when you combine API setup with UI verification — or vice versa:
Feature: Product Management
Scenario: Product created via API appears in admin UI
Given I create a product via API with name "Test Widget" and price 29.99
When I navigate to the admin product list
And I search for "Test Widget"
Then the product should appear in the list with price "$29.99"
Scenario: UI form submission creates correct API record
Given I am logged into the admin panel
When I fill in the product form with name "API Widget" and price 45.00
And I submit the product form
Then a GET request to "/products" should return a product named "API Widget"
And the stored price should be 45.00Step definitions mix SerenityRest for API calls and Serenity PageObject for UI:
public class ProductManagementSteps {
// UI page objects (auto-managed by Serenity)
AdminProductPage adminProductPage;
// API steps
@Steps
ProductApiSteps productApi;
@Steps
AuthApiSteps authApi;
@Given("I create a product via API with name {string} and price {double}")
public void createProductViaApi(String name, double price) {
String token = authApi.authenticateAndGetToken("admin@example.com", "AdminPass1!");
productApi.createProductWithToken(name, price, token);
}
@When("I navigate to the admin product list")
public void navigateToAdminProducts() {
adminProductPage.open();
}
@When("I search for {string}")
public void searchProducts(String query) {
adminProductPage.searchFor(query);
}
@Then("the product should appear in the list with price {string}")
public void verifyProductInList(String expectedPrice) {
adminProductPage.getProductPriceFor("Test Widget")
.shouldBeEqualTo(expectedPrice);
}
}Request/Response Logging in Reports
Serenity automatically captures REST Assured requests and responses in the HTML report when you use SerenityRest. Each API call appears in the step log with:
- Request method and URL
- Request headers and body
- Response status code
- Response headers and body (truncated for large payloads)
- Response time
This means every API failure in your report shows the exact request that failed and what the server returned — no digging through logs.
Configuration
Set base URLs per environment in serenity.conf:
environments {
default {
api.base.url = "https://api.staging.example.com"
webdriver.base.url = "https://staging.example.com"
}
production {
api.base.url = "https://api.example.com"
webdriver.base.url = "https://example.com"
}
}Read in step definitions:
String baseUrl = Serenity.environmentVariables()
.getProperty("api.base.url");Switch environments at runtime:
mvn verify -Denvironment=productionRunning and Reporting
# Run all tests
mvn verify
# Run only API tests
mvn verify -Dcucumber.filter.tags="@api"
# Run mixed tests
mvn verify -Dcucumber.filter.tags="@api or @ui"Reports in target/site/serenity/index.html show both API and UI tests in the same report, organized by feature. Product managers can see coverage across both layers without knowing which tests are API vs UI.
Adding Production Coverage
Serenity + Cucumber + REST Assured covers your acceptance test suite in CI. For production API monitoring, HelpMeTest runs continuous checks against your live endpoints — no Java, no build pipeline, no Cucumber required. When your API breaks in production, you know within minutes rather than at the next CI run.
Summary
SerenityRest is a drop-in replacement for REST Assured that logs every HTTP interaction into Serenity's HTML reports. Combined with Cucumber, you get acceptance tests that mix API calls and browser interactions in a single scenario, with a unified report showing step-level detail for both. Set it up once, and every test run produces a living document of your API behavior backed by real HTTP evidence.