# SkillPatch skill: serenity-bdd-skill

This skill generates Serenity BDD tests in Java using the Screenplay pattern, Step Library pattern, and Cucumber integration. It provides actionable code templates for writing tests, configuring rich HTML reporting, and executing tests on cloud platforms like LambdaTest via TestMu AI. It covers setup, test authoring, and cloud execution configuration end-to-end.

You (the agent) were given this URL and asked to use this skill. This is a **public** skill — no SkillPatch account, API key, or prior setup is required. Two ways to use it:

**1. Use it right now** — the skill's complete file tree (SKILL.md + all reference files) is inlined below; read `SKILL.md` first, then follow it, consulting the other files as it directs.

**2. Install the exact package onto disk** (recommended if you can run a shell — this reproduces the skill byte-for-byte, including any binary assets that can't be inlined):

```bash
mkdir -p .claude/skills/serenity-bdd-skill
curl -sSL https://skillpatch.dev/install_skill/serenity-bdd-skill | tar -xz -C .claude/skills/
```

(`.claude/skills/` is Claude Code's convention; use whatever directory your agent loads skills from.)


---

## Skill files (3)

- `SKILL.md`
- `reference/advanced-patterns.md`
- `reference/playbook.md`


### `SKILL.md`

````markdown
---
name: serenity-bdd-skill
description: >
  Generates Serenity BDD tests in Java with Screenplay pattern, rich reporting,
  and Cucumber integration. Use when user mentions "Serenity", "Screenplay",
  "@Steps", "Serenity BDD". Triggers on: "Serenity BDD", "Screenplay pattern",
  "@Steps", "Serenity report".
languages:
  - Java
category: bdd-testing
license: MIT
metadata:
  author: TestMu AI
  version: "1.0"
---

# Serenity BDD Skill

## Core Patterns

### Step Library Pattern

```java
import net.serenitybdd.annotations.Step;
import net.serenitybdd.core.pages.PageObject;

public class LoginSteps extends PageObject {

    @Step("Navigate to login page")
    public void navigateToLogin() {
        openUrl(getDriver().getCurrentUrl() + "/login");
    }

    @Step("Enter email: {0}")
    public void enterEmail(String email) {
        find(By.id("email")).sendKeys(email);
    }

    @Step("Enter password")
    public void enterPassword(String password) {
        find(By.id("password")).sendKeys(password);
    }

    @Step("Click login button")
    public void clickLogin() {
        find(By.cssSelector("button[type='submit']")).click();
    }

    @Step("Should see the dashboard")
    public void shouldSeeDashboard() {
        assertThat(getDriver().getCurrentUrl()).contains("/dashboard");
        assertThat(find(By.cssSelector(".welcome")).isDisplayed()).isTrue();
    }
}
```

### Test Class

```java
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.annotations.Steps;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;

@ExtendWith(SerenityJUnit5Extension.class)
public class LoginTest {
    @Steps LoginSteps loginSteps;

    @Test
    void shouldLoginWithValidCredentials() {
        loginSteps.navigateToLogin();
        loginSteps.enterEmail("user@test.com");
        loginSteps.enterPassword("password123");
        loginSteps.clickLogin();
        loginSteps.shouldSeeDashboard();
    }
}
```

### Screenplay Pattern

```java
import net.serenitybdd.screenplay.*;

public class Login implements Performable {
    private final String email, password;

    public Login(String email, String password) {
        this.email = email; this.password = password;
    }

    @Override
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
            Enter.theValue(email).into(LoginPage.EMAIL_FIELD),
            Enter.theValue(password).into(LoginPage.PASSWORD_FIELD),
            Click.on(LoginPage.LOGIN_BUTTON)
        );
    }

    public static Login withCredentials(String email, String password) {
        return new Login(email, password);
    }
}

// Usage
actor.attemptsTo(Login.withCredentials("user@test.com", "pass123"));
actor.should(seeThat(TheWebPage.currentUrl(), containsString("/dashboard")));
```

### Reporting

```bash
# Run tests — generates rich HTML report
mvn verify
# Report at: target/site/serenity/index.html
```

### Cloud Execution on TestMu AI

Add the `serenity-lambdatest` plugin dependency:

```xml
<dependency>
  <groupId>net.serenity-bdd</groupId>
  <artifactId>serenity-lambdatest</artifactId>
  <version>${serenity.version}</version>
</dependency>
```

Configure `serenity.conf`:

```hocon
webdriver {
  driver = remote
  remote.url = "https://"${LT_USERNAME}":"${LT_ACCESS_KEY}"@hub.lambdatest.com/wd/hub"
}

serenity {
  take.screenshots = AFTER_EACH_STEP
}

lambdatest {
  build = "Serenity Build"
}

# LT:Options capabilities
"LT:Options" {
  platformName = "Windows 11"
  browserVersion = "latest"
  visual = true
  video = true
  console = true
  network = true
}
```

Or configure via `serenity.properties`:

```properties
webdriver.driver=remote
webdriver.remote.url=https://hub.lambdatest.com/wd/hub
lt.user=${LT_USERNAME}
lt.key=${LT_ACCESS_KEY}
lt.platform=Windows 11
lt.browserName=chrome
```

## Setup: Maven with `serenity-core`, `serenity-junit5`, `serenity-screenplay-webdriver`, `serenity-lambdatest`
## Run: `mvn verify` (generates living documentation)

## Deep Patterns

For advanced patterns, debugging guides, CI/CD integration, and best practices,
see `reference/playbook.md`.

````


### `reference/advanced-patterns.md`

````markdown
# Serenity BDD — Advanced Patterns & Playbook

## Screenplay Pattern

```java
import net.serenitybdd.screenplay.*;
import net.serenitybdd.screenplay.actions.*;
import net.serenitybdd.screenplay.questions.*;

public class Login implements Task {
    private final String username, password;
    public Login(String username, String password) {
        this.username = username; this.password = password;
    }

    @Override
    @Step("{0} logs in as #username")
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
            Navigate.to("/login"),
            Enter.theValue(username).into("#username"),
            Enter.theValue(password).into("#password"),
            Click.on("#submit")
        );
    }

    public static Login withCredentials(String user, String pass) {
        return new Login(user, pass);
    }
}

// Question
public class CurrentUser implements Question<String> {
    @Override
    public String answeredBy(Actor actor) {
        return Text.of("#user-display").answeredBy(actor);
    }
    public static Question<String> name() { return new CurrentUser(); }
}

// Test
@Test
void adminCanLogin() {
    Actor admin = Actor.named("Admin");
    admin.attemptsTo(Login.withCredentials("admin", "pass"));
    admin.should(seeThat(CurrentUser.name(), equalTo("Admin")));
}
```

## REST API Testing

```java
@Steps SerenityRest restSteps;

@Test
void createUser() {
    given().contentType(JSON).body(new User("Alice", "alice@test.com"))
        .when().post("/api/users")
        .then().statusCode(201)
        .body("name", equalTo("Alice"));

    // Chained API verification
    String id = lastResponse().jsonPath().getString("id");
    given().when().get("/api/users/" + id)
        .then().statusCode(200)
        .body("name", equalTo("Alice"));
}
```

## Living Documentation

```java
// Feature narrative in Cucumber
@Narrative(text = {"As a customer", "I want to manage my cart",
    "So that I can purchase items"})
@WithTags({@WithTag("cart"), @WithTag("smoke")})
public class CartTest extends SerenityStory {
    @Test @Title("Add item to cart and verify total")
    void addToCart() { /* ... */ }
}
```

## Anti-Patterns

- ❌ Direct WebDriver calls — use Screenplay Tasks and Actions
- ❌ Assertions in Task classes — use Questions for verification
- ❌ Skipping `@Step` annotations — breaks living documentation reports
- ❌ Fat step definitions — delegate to reusable Task objects

````


### `reference/playbook.md`

````markdown
# Serenity BDD — Advanced Playbook

## §1 — Project Setup

### Maven Configuration
```xml
<!-- pom.xml -->
<project>
    <properties>
        <serenity.version>4.1.4</serenity.version>
        <serenity.maven.version>4.1.4</serenity.maven.version>
        <junit.version>5.10.2</junit.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-core</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-junit5</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-screenplay</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-screenplay-webdriver</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-ensure</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-rest-assured</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>net.serenity-bdd</groupId>
            <artifactId>serenity-cucumber</artifactId>
            <version>${serenity.version}</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>net.serenity-bdd.maven.plugins</groupId>
                <artifactId>serenity-maven-plugin</artifactId>
                <version>${serenity.maven.version}</version>
                <executions>
                    <execution>
                        <id>serenity-reports</id>
                        <phase>post-integration-test</phase>
                        <goals><goal>aggregate</goal></goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>
```

### Project Structure
```
src/test/java/
├── features/
│   ├── login/
│   │   ├── LoginTest.java
│   │   └── LoginWithCucumber.java
│   └── search/
│       └── SearchTest.java
├── screenplay/
│   ├── abilities/
│   │   └── AuthenticateWithAPI.java
│   ├── actions/
│   │   ├── LoginActions.java
│   │   ├── NavigateTo.java
│   │   └── Search.java
│   ├── questions/
│   │   ├── DashboardInfo.java
│   │   └── SearchResults.java
│   └── tasks/
│       ├── Login.java
│       ├── PlaceOrder.java
│       └── SearchForProduct.java
├── pages/
│   ├── LoginPage.java
│   ├── DashboardPage.java
│   └── SearchResultsPage.java
├── steps/
│   ├── LoginSteps.java
│   ├── NavigationSteps.java
│   └── SearchSteps.java
└── stepdefs/
    └── LoginStepDefinitions.java
src/test/resources/
├── serenity.conf
└── features/
    └── login.feature
```

### serenity.conf
```hocon
serenity {
    project.name = "My Project Tests"
    test.root = "features"
    tag.failures = "true"
    take.screenshots = FOR_FAILURES
    browser.maximized = true
    restart.browser.for.each = SCENARIO
}

headless.mode = false

webdriver {
    driver = chrome
    autodownload = true

    capabilities {
        browserName = "chrome"
        "goog:chromeOptions" {
            args = ["--remote-allow-origins=*", "--disable-gpu", "--no-sandbox",
                    "--disable-dev-shm-usage", "--window-size=1920,1080"]
        }
    }
}

environments {
    default {
        webdriver.base.url = "http://localhost:3000"
        api.base.url = "http://localhost:3000/api"
    }
    staging {
        webdriver.base.url = "https://staging.example.com"
        api.base.url = "https://staging.example.com/api"
    }
    lambdatest {
        webdriver {
            driver = remote
            remote.url = "https://"${LT_USERNAME}":"${LT_ACCESS_KEY}"@hub.lambdatest.com/wd/hub"
            capabilities {
                browserName = "chrome"
                "LT:Options" {
                    build = "serenity-"${BUILD_ID}
                    name = "Serenity BDD Tests"
                    platform = "Windows 11"
                    resolution = "1920x1080"
                    network = true
                    video = true
                    console = true
                    visual = true
                }
            }
        }
    }
}
```

---

## §2 — Step Libraries Pattern

### Step Library Classes
```java
public class LoginSteps extends ScenarioSteps {
    LoginPage loginPage;

    @Step("Navigate to the login page")
    public void navigateToLogin() {
        loginPage.open();
    }

    @Step("Enter credentials for {0}")
    public void enterCredentials(String email, String password) {
        loginPage.enterEmail(email);
        loginPage.enterPassword(password);
    }

    @Step("Click the login button")
    public void clickLogin() {
        loginPage.clickLogin();
    }

    @Step("Login as {0}")
    public void loginAs(String email, String password) {
        navigateToLogin();
        enterCredentials(email, password);
        clickLogin();
    }

    @Step("Verify error message contains '{0}'")
    public void verifyErrorMessage(String expected) {
        assertThat(loginPage.getErrorMessage()).containsIgnoringCase(expected);
    }
}

public class NavigationSteps extends ScenarioSteps {
    DashboardPage dashboardPage;

    @Step("Verify dashboard is displayed")
    public void verifyDashboard() {
        dashboardPage.shouldBeDisplayed();
    }

    @Step("Verify welcome message for '{0}'")
    public void verifyWelcomeMessage(String name) {
        assertThat(dashboardPage.getWelcomeMessage()).contains(name);
    }

    @Step("Navigate to {0} from sidebar")
    public void navigateToSection(String section) {
        dashboardPage.clickSidebarLink(section);
    }
}
```

### Page Objects
```java
@DefaultUrl("/login")
public class LoginPage extends PageObject {

    @FindBy(css = "[data-testid='email']")
    private WebElementFacade emailInput;

    @FindBy(css = "[data-testid='password']")
    private WebElementFacade passwordInput;

    @FindBy(css = "button[type='submit']")
    private WebElementFacade loginButton;

    @FindBy(css = "[data-testid='error-message']")
    private WebElementFacade errorMessage;

    public void enterEmail(String email) {
        emailInput.waitUntilVisible().clear();
        emailInput.type(email);
    }

    public void enterPassword(String password) {
        passwordInput.waitUntilVisible().clear();
        passwordInput.type(password);
    }

    public void clickLogin() {
        loginButton.waitUntilClickable().click();
    }

    public String getErrorMessage() {
        return errorMessage.waitUntilVisible().getText();
    }

    public boolean isErrorDisplayed() {
        return errorMessage.isCurrentlyVisible();
    }
}

@DefaultUrl("/dashboard")
public class DashboardPage extends PageObject {

    @FindBy(css = "[data-testid='welcome']")
    private WebElementFacade welcomeMessage;

    @FindBy(css = ".sidebar-nav a")
    private List<WebElementFacade> sidebarLinks;

    public void shouldBeDisplayed() {
        welcomeMessage.shouldBeVisible();
    }

    public String getWelcomeMessage() {
        return welcomeMessage.waitUntilVisible().getText();
    }

    public void clickSidebarLink(String text) {
        sidebarLinks.stream()
            .filter(link -> link.getText().equalsIgnoreCase(text))
            .findFirst()
            .orElseThrow(() -> new NoSuchElementException("Link not found: " + text))
            .click();
    }
}
```

### Tests Using Step Libraries
```java
@ExtendWith(SerenityJUnit5Extension.class)
@Tag("smoke")
class LoginTest {
    @Steps LoginSteps loginSteps;
    @Steps NavigationSteps navigationSteps;

    @Test
    @Title("Successful login with valid credentials")
    void successfulLogin() {
        loginSteps.loginAs("admin@test.com", "password");
        navigationSteps.verifyDashboard();
        navigationSteps.verifyWelcomeMessage("Admin");
    }

    @Test
    @Title("Login fails with invalid credentials")
    void loginWithInvalidCredentials() {
        loginSteps.loginAs("wrong@test.com", "bad");
        loginSteps.verifyErrorMessage("Invalid credentials");
    }

    @Test
    @Title("Login with empty fields shows validation")
    void loginWithEmptyFields() {
        loginSteps.navigateToLogin();
        loginSteps.clickLogin();
        loginSteps.verifyErrorMessage("required");
    }
}
```

---

## §3 — Screenplay Pattern

### Tasks
```java
public class Login implements Task {
    private final String email;
    private final String password;

    public Login(String email, String password) {
        this.email = email;
        this.password = password;
    }

    public static Login withCredentials(String email, String password) {
        return new Login(email, password);
    }

    @Override
    @Step("{0} logs in with #email")
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
            NavigateTo.theLoginPage(),
            Enter.theValue(email).into(LoginPageElements.EMAIL_FIELD),
            Enter.theValue(password).into(LoginPageElements.PASSWORD_FIELD),
            Click.on(LoginPageElements.LOGIN_BUTTON)
        );
    }
}

public class NavigateTo {
    public static Performable theLoginPage() {
        return Task.where("{0} navigates to the login page",
            Open.url("/login")
        );
    }

    public static Performable theSearchPage() {
        return Task.where("{0} navigates to search",
            Open.url("/search")
        );
    }
}

public class SearchForProduct implements Task {
    private final String query;

    public SearchForProduct(String query) { this.query = query; }

    public static SearchForProduct called(String query) {
        return new SearchForProduct(query);
    }

    @Override
    @Step("{0} searches for '#query'")
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
            Enter.theValue(query).into(SearchPageElements.SEARCH_INPUT).thenHit(Keys.ENTER),
            WaitUntil.the(SearchPageElements.RESULTS_CONTAINER, isVisible())
                .forNoMoreThan(10).seconds()
        );
    }
}
```

### Questions
```java
public class SearchResults {

    public static Question<Integer> count() {
        return actor -> {
            return SearchPageElements.RESULT_ITEMS
                .resolveAllFor(actor)
                .size();
        };
    }

    public static Question<List<String>> titles() {
        return actor -> {
            return SearchPageElements.RESULT_TITLES
                .resolveAllFor(actor)
                .stream()
                .map(WebElementFacade::getText)
                .collect(Collectors.toList());
        };
    }

    public static Question<String> firstResultTitle() {
        return actor -> {
            return SearchPageElements.RESULT_TITLES
                .resolveAllFor(actor)
                .get(0)
                .getText();
        };
    }
}

public class DashboardInfo {

    public static Question<String> welcomeMessage() {
        return actor -> DashboardPageElements.WELCOME_MESSAGE
            .resolveFor(actor)
            .getText();
    }

    public static Question<Boolean> isDisplayed() {
        return actor -> DashboardPageElements.WELCOME_MESSAGE
            .resolveFor(actor)
            .isCurrentlyVisible();
    }
}
```

### Page Elements (Targets)
```java
public class LoginPageElements {
    public static final Target EMAIL_FIELD =
        Target.the("email field").locatedBy("[data-testid='email']");
    public static final Target PASSWORD_FIELD =
        Target.the("password field").locatedBy("[data-testid='password']");
    public static final Target LOGIN_BUTTON =
        Target.the("login button").locatedBy("button[type='submit']");
    public static final Target ERROR_MESSAGE =
        Target.the("error message").locatedBy("[data-testid='error-message']");
}

public class SearchPageElements {
    public static final Target SEARCH_INPUT =
        Target.the("search input").locatedBy("input#search");
    public static final Target RESULTS_CONTAINER =
        Target.the("results container").locatedBy(".search-results");
    public static final Target RESULT_ITEMS =
        Target.the("result items").locatedBy(".search-result-item");
    public static final Target RESULT_TITLES =
        Target.the("result titles").locatedBy(".search-result-item h3");
}
```

### Screenplay Tests
```java
@ExtendWith(SerenityJUnit5Extension.class)
class SearchScreenplayTest {

    Actor alice = Actor.named("Alice");

    @BeforeEach
    void setup() {
        alice.can(BrowseTheWeb.with(getDriver()));
    }

    @Test
    @Title("Search returns matching results")
    void searchReturnsResults() {
        alice.attemptsTo(
            NavigateTo.theSearchPage(),
            SearchForProduct.called("laptop")
        );

        alice.should(
            seeThat(SearchResults.count(), greaterThan(0)),
            seeThat(SearchResults.firstResultTitle(), containsString("Laptop"))
        );
    }

    @Test
    @Title("Login and verify dashboard")
    void loginAndVerifyDashboard() {
        alice.attemptsTo(
            Login.withCredentials("admin@test.com", "password")
        );

        alice.should(
            seeThat(DashboardInfo.isDisplayed(), is(true)),
            seeThat(DashboardInfo.welcomeMessage(), containsString("Admin"))
        );
    }
}
```

---

## §4 — Cucumber Integration

### Feature Files
```gherkin
# src/test/resources/features/login.feature
@login
Feature: User Login
  As a registered user
  I want to log into the application
  So that I can access my dashboard

  Background:
    Given I am on the login page

  @smoke @critical
  Scenario: Successful login with valid credentials
    When I login with "admin@test.com" and "password"
    Then I should see the dashboard
    And the welcome message should contain "Admin"

  @negative
  Scenario Outline: Login with invalid credentials
    When I login with "<email>" and "<password>"
    Then I should see an error message containing "<error>"

    Examples:
      | email            | password | error               |
      | wrong@test.com   | bad      | Invalid credentials |
      | admin@test.com   | wrong    | Invalid credentials |
      |                  |          | required            |
```

### Step Definitions
```java
public class LoginStepDefinitions {

    @Steps LoginSteps loginSteps;
    @Steps NavigationSteps navigationSteps;

    @Given("I am on the login page")
    public void iAmOnTheLoginPage() {
        loginSteps.navigateToLogin();
    }

    @When("I login with {string} and {string}")
    public void iLoginWith(String email, String password) {
        loginSteps.enterCredentials(email, password);
        loginSteps.clickLogin();
    }

    @Then("I should see the dashboard")
    public void iShouldSeeTheDashboard() {
        navigationSteps.verifyDashboard();
    }

    @Then("the welcome message should contain {string}")
    public void welcomeMessageContains(String expected) {
        navigationSteps.verifyWelcomeMessage(expected);
    }

    @Then("I should see an error message containing {string}")
    public void errorMessageContaining(String expected) {
        loginSteps.verifyErrorMessage(expected);
    }
}
```

### Cucumber Runner
```java
@Suite
@IncludeEngines("cucumber")
@SelectClasspathResource("/features")
@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME,
    value = "io.cucumber.core.plugin.SerenityReporterParallelPlugin,pretty")
@ConfigurationParameter(key = GLUE_PROPERTY_NAME,
    value = "stepdefs")
public class CucumberTestRunner {}
```

---

## §5 — REST API Testing

### Serenity REST Integration
```java
@ExtendWith(SerenityJUnit5Extension.class)
class ApiTest {
    @Steps ApiSteps apiSteps;

    @Test
    @Title("Create user via API")
    void createUser() {
        apiSteps.createUser("John", "john@test.com");
        apiSteps.verifyStatusCode(201);
        apiSteps.verifyResponseContains("id");
    }

    @Test
    @Title("List users returns paginated results")
    void listUsers() {
        apiSteps.getUsers(1, 10);
        apiSteps.verifyStatusCode(200);
        apiSteps.verifyUserCount(10);
    }
}

public class ApiSteps extends ScenarioSteps {
    private final String baseUrl = EnvironmentVariables
        .from(ConfiguredEnvironment.getEnvironmentVariables())
        .getProperty("api.base.url");

    @Step("Create user with name '{0}' and email '{1}'")
    public void createUser(String name, String email) {
        SerenityRest.given()
            .baseUri(baseUrl)
            .contentType(ContentType.JSON)
            .body(Map.of("name", name, "email", email))
        .when()
            .post("/users")
        .then()
            .log().ifError();
    }

    @Step("GET users page {0}, size {1}")
    public void getUsers(int page, int size) {
        SerenityRest.given()
            .baseUri(baseUrl)
            .queryParam("page", page)
            .queryParam("size", size)
        .when()
            .get("/users");
    }

    @Step("Verify status code is {0}")
    public void verifyStatusCode(int expected) {
        SerenityRest.then().statusCode(expected);
    }

    @Step("Verify response contains field '{0}'")
    public void verifyResponseContains(String field) {
        SerenityRest.then().body(field, notNullValue());
    }

    @Step("Verify {0} users returned")
    public void verifyUserCount(int count) {
        SerenityRest.then().body("data.size()", equalTo(count));
    }
}
```

---

## §6 — Reporting & Tags

### Custom Tags and Reporting
```java
@ExtendWith(SerenityJUnit5Extension.class)
class TaggedTests {

    @Test
    @Title("Critical checkout flow")
    @WithTag("type:smoke")
    @WithTags({
        @WithTag("feature:checkout"),
        @WithTag("priority:critical"),
        @WithTag("sprint:24")
    })
    void criticalCheckout() {
        // test implementation
    }

    @Test
    @Title("User profile update")
    @WithTag("feature:profile")
    @Narrative(text = {
        "As a logged-in user",
        "I want to update my profile",
        "So that my information is current"
    })
    void updateProfile() {
        // test implementation
    }

    @Test
    @Pending
    @Title("Feature not yet implemented")
    void pendingFeature() {
        // Will appear as pending in reports
    }

    @Test
    @Manual
    @Title("Manual verification required")
    void manualTest() {
        // Tracked in reports but not automated
    }
}
```

### Running by Tags
```bash
# Run only smoke tests
mvn verify -Dtags="type:smoke"

# Run specific feature
mvn verify -Dtags="feature:checkout"

# Exclude pending
mvn verify -Dtags="not @Pending"

# Combine tags
mvn verify -Dtags="type:smoke and priority:critical"
```

---

## §7 — LambdaTest Integration

### Environment-Based Configuration
```hocon
# In serenity.conf - environments section
environments {
    lambdatest {
    
...<truncated>
````
