Cucumber with Rest Assured for API BDD

9 min read

BDD doesn't stop at the browser. The same Gherkin feature files that describe UI behaviour can describe API behaviour — and the non-technical readability benefit applies just as strongly. A product owner can review When I create a user via POST /users and verify it matches their understanding of the API contract, without knowing what RequestSpecification means.

This lesson wires Cucumber to Rest Assured. The feature files and runner are identical to UI scenarios. The step definitions call Rest Assured instead of Selenium.

API BDD feature files

@api
Feature: User Management API
 
  Background:
    Given the API is available at the base URL
 
  @smoke
  Scenario: Create a new user successfully
    When I send a POST request to "/users" with:
      | name  | Alice Smith    |
      | email | alice@test.com |
      | role  | admin          |
    Then the response status code should be 201
    And the response should contain field "id"
    And the response field "name" should equal "Alice Smith"
 
  Scenario: Get a user by ID
    Given a user exists with name "Bob" and email "bob@test.com"
    When I send a GET request to "/users/{userId}"
    Then the response status code should be 200
    And the response field "name" should equal "Bob"
 
  Scenario: Delete a user
    Given a user exists with name "Carol" and email "carol@test.com"
    When I send a DELETE request to "/users/{userId}"
    Then the response status code should be 204

Notice the {userId} in the path. The Given a user exists... step creates the user, stores the returned ID in TestContext, and the subsequent When step retrieves it. This is the scenario-context pattern from the DI lesson applied to API testing.

The TestContext for API state

public class TestContext {
    // ... existing driver, authToken fields ...
    private Response lastResponse;
    private String lastCreatedId;
 
    public Response getLastResponse() { return lastResponse; }
    public void setLastResponse(Response response) { this.lastResponse = response; }
 
    public String getLastCreatedId() { return lastCreatedId; }
    public void setLastCreatedId(String id) { this.lastCreatedId = id; }
}

Storing the Response object in context lets any subsequent step access the status code, headers, or body — regardless of which step definition class makes the assertion.

API step definitions with Rest Assured

package stepdefinitions;
 
import context.TestContext;
import io.cucumber.datatable.DataTable;
import io.cucumber.java.en.And;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import io.restassured.http.ContentType;
import io.restassured.response.Response;
 
import java.util.Map;
import static io.restassured.RestAssured.*;
import static org.junit.jupiter.api.Assertions.*;
 
public class UserApiSteps {
    private final TestContext context;
 
    public UserApiSteps(TestContext context) {
        this.context = context;
    }
 
    @Given("the API is available at the base URL")
    public void apiIsAvailable() {
        baseURI = System.getProperty("apiBaseUrl", "https://reqres.in/api");
    }
 
    @When("I send a POST request to {string} with:")
    public void postWithBody(String endpoint, DataTable dataTable) {
        Map<String, String> body = dataTable.asMap();
        Response response = given()
            .contentType(ContentType.JSON)
            .body(body)
        .when()
            .post(endpoint);
        context.setLastResponse(response);
        if (response.getStatusCode() == 201) {
            context.setLastCreatedId(response.jsonPath().getString("id"));
        }
    }
 
    @When("I send a GET request to {string}")
    public void getRequest(String endpoint) {
        String resolvedPath = endpoint.replace("{userId}", context.getLastCreatedId());
        Response response = given()
        .when()
            .get(resolvedPath);
        context.setLastResponse(response);
    }
 
    @When("I send a DELETE request to {string}")
    public void deleteRequest(String endpoint) {
        String resolvedPath = endpoint.replace("{userId}", context.getLastCreatedId());
        Response response = given()
        .when()
            .delete(resolvedPath);
        context.setLastResponse(response);
    }
 
    @Given("a user exists with name {string} and email {string}")
    public void createUserForTest(String name, String email) {
        Response response = given()
            .contentType(ContentType.JSON)
            .body(Map.of("name", name, "email", email))
        .when()
            .post("/users");
        context.setLastCreatedId(response.jsonPath().getString("id"));
    }
 
    @Then("the response status code should be {int}")
    public void verifyStatusCode(int expected) {
        assertEquals(expected, context.getLastResponse().getStatusCode(),
            "Unexpected status code. Body: " + context.getLastResponse().getBody().asString());
    }
 
    @And("the response should contain field {string}")
    public void responseContainsField(String fieldName) {
        assertNotNull(context.getLastResponse().jsonPath().get(fieldName),
            "Field '" + fieldName + "' was not found in response: "
            + context.getLastResponse().getBody().asString());
    }
 
    @And("the response field {string} should equal {string}")
    public void responseFieldEquals(String field, String expected) {
        String actual = context.getLastResponse().jsonPath().getString(field);
        assertEquals(expected, actual,
            "Field '" + field + "' expected '" + expected + "' but was '" + actual + "'");
    }
}

Mixing UI and API steps in one scenario

The real power of a unified BDD framework: you can combine UI and API steps in a single scenario when the test requires it:

@ui @api
Scenario: User created via API appears in the admin dashboard
  Given a user is created via API with name "Alice" and email "alice@test.com"
  When the admin navigates to the user management page
  Then "Alice" should appear in the user list

The Given step runs a Rest Assured POST. The When and Then steps use Selenium. Both step definition classes receive the same TestContext. The @Before("@ui") hook starts the browser; the API step fires without browser overhead. Tags @ui @api ensure both setup hooks run.

This pattern is particularly useful for test data setup — creating the API state programmatically is faster and more reliable than clicking through a UI to create it.

UI BDD vs API BDD — same Gherkin, different step implementations

Cucumber + Selenium (UI BDD)

  • Tag: @ui

  • @Before: starts ChromeDriver

  • Step definitions call Page Objects

  • Page Objects call WebDriver APIs

  • driver.findElement(), sendKeys(), click()

  • WebDriverWait for async content

  • Screenshot on failure via Scenario.attach()

  • Slower (seconds per scenario)

Cucumber + Rest Assured (API BDD)

  • Tag: @api

  • @Before: sets RestAssured.baseURI

  • Step definitions call given().when().then()

  • Response stored in TestContext

  • jsonPath().getString(), statusCode()

  • No waits needed — synchronous HTTP

  • Response body logged on failure

  • Faster (milliseconds per scenario)

⚠️ Common mistakes

  • Storing Response as a local variable in the @When step, then trying to access it in @Then. Local variables don't survive across step method calls — store Response in TestContext and retrieve it in subsequent steps.
  • Hardcoding the base URL in each step definition. given().baseUri("https://prod.api.myapp.com") couples tests to a specific environment. Read the base URL from a system property (System.getProperty("apiBaseUrl")) or an environment variable, passed via Maven -DapiBaseUrl=... or CI environment config.
  • Not enabling failure logging. RestAssured.enableLoggingOfRequestAndResponseIfValidationFails() should be in the @Before("@api") hook. Without it, a failed status code assertion gives you "Expected 200 but was 500" with no body — not enough to debug in CI.
  • Sharing a RequestSpecification static field. Like the driver, a static RequestSpec persists across scenarios. Build it fresh per scenario (in the @Before hook or the first Given step) and store it in TestContext.

🎯 Practice task

Write a complete API BDD suite against a public test API. 40–50 minutes. Use reqres.in — a free API that supports GET, POST, PUT, DELETE.

  1. Add rest-assured to your pom.xml. Create UserApiSteps.java using the code from this lesson.
  2. Write users-api.feature with 4 scenarios: create user, get user, update user (PUT), delete user. Tag them all @api.
  3. Add @Before("@api") to Hooks.java that sets RestAssured.baseURI = "https://reqres.in/api" and enables failure logging.
  4. Run mvn test -Dcucumber.filter.tags="@api" — all 4 scenarios should pass.
  5. Deliberately assert the wrong status code (e.g., expect 200 for a 201 response). Run again — confirm the failure message includes the response body.
  6. Write a fifth scenario that chains two API calls: Given a user exists (POST) → When I get the user by ID (GET) → Then the name should match. Confirm the ID is passed correctly through TestContext.
  7. Stretch: write a scenario tagged both @ui and @api: create a user via the API in Given, then navigate to a hypothetical admin page in When/Then using a page object. Even if the admin UI doesn't exist, verify that both the API hook (@Before("@api")) and the UI hook (@Before("@ui")) both fire for a scenario with both tags.

Next lesson: running Cucumber scenarios in parallel for faster suite execution.

// tip to track lessons you complete and pick up where you left off across devices.