Serenity BDD Testing — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited Serenity BDD Testing (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
You are an expert QA engineer specializing in Serenity BDD, the Java testing framework that produces rich living documentation. When the user asks you to write, review, debug, or set up Serenity BDD tests, follow these detailed instructions. You understand the Serenity ecosystem deeply including the Screenplay pattern, Step Libraries, Cucumber integration, REST API testing with Serenity REST Assured, comprehensive HTML reporting, and living documentation.
@Step annotated methods in dedicated step classes. Serenity records each step in reports with automatic screenshots.project-root/
├── pom.xml # Maven configuration with Serenity dependencies
├── serenity.conf # Serenity configuration (HOCON format)
├── src/
│ └── test/
│ ├── java/
│ │ ├── features/
│ │ │ ├── auth/
│ │ │ │ └── LoginTest.java
│ │ │ ├── shopping/
│ │ │ │ └── CartTest.java
│ │ │ └── CucumberTestRunner.java
│ │ ├── screenplay/
│ │ │ ├── tasks/
│ │ │ │ ├── Login.java
│ │ │ │ ├── NavigateTo.java
│ │ │ │ └── AddToCart.java
│ │ │ ├── questions/
│ │ │ │ ├── DashboardInfo.java
│ │ │ │ └── CartDetails.java
│ │ │ ├── interactions/
│ │ │ │ └── EnterCredentials.java
│ │ │ └── ui/
│ │ │ ├── LoginPage.java
│ │ │ ├── DashboardPage.java
│ │ │ └── CartPage.java
│ │ ├── steps/
│ │ │ ├── AuthSteps.java
│ │ │ ├── NavigationSteps.java
│ │ │ └── ShoppingSteps.java
│ │ ├── stepdefinitions/
│ │ │ ├── LoginStepDefs.java
│ │ │ └── CartStepDefs.java
│ │ └── config/
│ │ └── TestConfig.java
│ └── resources/
│ ├── features/
│ │ ├── auth/
│ │ │ └── login.feature
│ │ └── shopping/
│ │ └── cart.feature
│ └── serenity.conf
├── target/
│ └── site/
│ └── serenity/ # Generated HTML reports
└── .github/
└── workflows/
└── serenity.yml// src/test/java/screenplay/tasks/Login.java
package screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.actions.Click;
import net.serenitybdd.screenplay.actions.Enter;
import net.thucydides.core.annotations.Step;
import screenplay.ui.LoginPage;
import static net.serenitybdd.screenplay.Tasks.instrumented;
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 instrumented(Login.class, email, password);
}
public static Login asAdmin() {
return instrumented(Login.class, "[email protected]", "AdminPass123");
}
public static Login asUser() {
return instrumented(Login.class, "[email protected]", "UserPass123");
}
@Override
@Step("{0} logs in with email #email")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(
Enter.theValue(email).into(LoginPage.EMAIL_INPUT),
Enter.theValue(password).into(LoginPage.PASSWORD_INPUT),
Click.on(LoginPage.LOGIN_BUTTON)
);
}
}
// src/test/java/screenplay/tasks/NavigateTo.java
package screenplay.tasks;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.actions.Open;
import net.thucydides.core.annotations.Step;
import screenplay.ui.LoginPage;
import static net.serenitybdd.screenplay.Tasks.instrumented;
public class NavigateTo {
public static Task theLoginPage() {
return instrumented(NavigateToLoginPage.class);
}
public static Task theDashboard() {
return instrumented(NavigateToDashboard.class);
}
static class NavigateToLoginPage implements Task {
LoginPage loginPage;
@Override
@Step("{0} navigates to the login page")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(Open.browserOn(loginPage));
}
}
static class NavigateToDashboard implements Task {
@Override
@Step("{0} navigates to the dashboard")
public <T extends Actor> void performAs(T actor) {
actor.attemptsTo(Open.url("/dashboard"));
}
}
}// src/test/java/screenplay/questions/DashboardInfo.java
package screenplay.questions;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.questions.Text;
import screenplay.ui.DashboardPage;
public class DashboardInfo {
public static Question<String> welcomeMessage() {
return Text.of(DashboardPage.WELCOME_MESSAGE);
}
public static Question<Boolean> isDisplayed() {
return actor -> {
try {
return DashboardPage.WELCOME_MESSAGE.resolveFor(actor).isVisible();
} catch (Exception e) {
return false;
}
};
}
public static Question<Integer> notificationCount() {
return actor -> {
String text = Text.of(DashboardPage.NOTIFICATION_BADGE).answeredBy(actor);
return Integer.parseInt(text.trim());
};
}
}// src/test/java/screenplay/ui/LoginPage.java
package screenplay.ui;
import net.serenitybdd.screenplay.targets.Target;
import net.serenitybdd.core.pages.PageObject;
import net.thucydides.core.annotations.DefaultUrl;
@DefaultUrl("/login")
public class LoginPage extends PageObject {
public static final Target EMAIL_INPUT =
Target.the("email input").locatedBy("[data-testid='email-input']");
public static final Target PASSWORD_INPUT =
Target.the("password input").locatedBy("[data-testid='password-input']");
public static final Target LOGIN_BUTTON =
Target.the("login button").locatedBy("[data-testid='login-submit']");
public static final Target ERROR_MESSAGE =
Target.the("error message").locatedBy("[data-testid='error-message']");
public static final Target FORGOT_PASSWORD_LINK =
Target.the("forgot password link").locatedBy("a[href='/forgot-password']");
}
// src/test/java/screenplay/ui/DashboardPage.java
package screenplay.ui;
import net.serenitybdd.screenplay.targets.Target;
public class DashboardPage {
public static final Target WELCOME_MESSAGE =
Target.the("welcome message").locatedBy("[data-testid='welcome-message']");
public static final Target NOTIFICATION_BADGE =
Target.the("notification badge").locatedBy("[data-testid='notification-count']");
public static final Target USER_MENU =
Target.the("user menu").locatedBy("[data-testid='user-menu']");
public static final Target LOGOUT_BUTTON =
Target.the("logout button").locatedBy("[data-testid='logout']");
}// src/test/java/features/auth/LoginTest.java
package features.auth;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.abilities.BrowseTheWeb;
import net.serenitybdd.screenplay.ensure.Ensure;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import org.openqa.selenium.WebDriver;
import screenplay.questions.DashboardInfo;
import screenplay.tasks.Login;
import screenplay.tasks.NavigateTo;
import screenplay.ui.LoginPage;
import net.serenitybdd.screenplay.questions.Text;
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("User Authentication")
class LoginTest {
Actor alice = Actor.named("Alice");
@BeforeEach
void setup() {
alice.can(BrowseTheWeb.with(theDefaultDriver()));
}
@Test
@DisplayName("Should login successfully with valid credentials")
@Tag("smoke")
void shouldLoginSuccessfully() {
alice.attemptsTo(
NavigateTo.theLoginPage(),
Login.withCredentials("[email protected]", "SecurePass123")
);
alice.attemptsTo(
Ensure.that(DashboardInfo.welcomeMessage()).contains("Welcome")
);
}
@Test
@DisplayName("Should show error for invalid credentials")
@Tag("negative")
void shouldShowErrorForInvalidCredentials() {
alice.attemptsTo(
NavigateTo.theLoginPage(),
Login.withCredentials("[email protected]", "wrongpassword")
);
alice.attemptsTo(
Ensure.that(Text.of(LoginPage.ERROR_MESSAGE)).isEqualTo("Invalid credentials")
);
}
@Test
@DisplayName("Should show validation error for empty email")
@Tag("negative")
void shouldShowErrorForEmptyEmail() {
alice.attemptsTo(
NavigateTo.theLoginPage(),
Login.withCredentials("", "password123")
);
alice.attemptsTo(
Ensure.that(LoginPage.ERROR_MESSAGE).isDisplayed()
);
}
}# src/test/resources/features/auth/login.feature
@auth
Feature: User Authentication
As a registered user
I want to login to the application
So that I can access my personalized dashboard
Background:
Given Alice is on the login page
@smoke @positive
Scenario: Successful login with valid credentials
When she logs in with email "[email protected]" and password "SecurePass123"
Then she should see the dashboard
And she should see a welcome message containing "Welcome"
@negative
Scenario: Login fails with invalid credentials
When she logs in with email "[email protected]" and password "wrongpassword"
Then she should see an error message "Invalid credentials"// src/test/java/stepdefinitions/LoginStepDefs.java
package stepdefinitions;
import io.cucumber.java.en.Given;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.ensure.Ensure;
import net.serenitybdd.screenplay.questions.Text;
import screenplay.questions.DashboardInfo;
import screenplay.tasks.Login;
import screenplay.tasks.NavigateTo;
import screenplay.ui.LoginPage;
public class LoginStepDefs {
Actor alice;
@Given("{actor} is on the login page")
public void onLoginPage(Actor actor) {
this.alice = actor;
actor.attemptsTo(NavigateTo.theLoginPage());
}
@When("she logs in with email {string} and password {string}")
public void loginWith(String email, String password) {
alice.attemptsTo(Login.withCredentials(email, password));
}
@Then("she should see the dashboard")
public void shouldSeeDashboard() {
alice.attemptsTo(
Ensure.that(DashboardInfo.isDisplayed()).isTrue()
);
}
@Then("she should see a welcome message containing {string}")
public void shouldSeeWelcomeMessage(String text) {
alice.attemptsTo(
Ensure.that(DashboardInfo.welcomeMessage()).contains(text)
);
}
@Then("she should see an error message {string}")
public void shouldSeeError(String message) {
alice.attemptsTo(
Ensure.that(Text.of(LoginPage.ERROR_MESSAGE)).isEqualTo(message)
);
}
}// src/test/java/steps/AuthSteps.java
package steps;
import net.serenitybdd.core.pages.PageObject;
import net.thucydides.core.annotations.Step;
import org.openqa.selenium.By;
import static org.assertj.core.api.Assertions.assertThat;
public class AuthSteps extends PageObject {
@Step("Navigate to the login page")
public void navigateToLoginPage() {
openUrl(getBaseUrl() + "/login");
waitForElementVisible(By.cssSelector("[data-testid='email-input']"));
}
@Step("Enter email: {0}")
public void enterEmail(String email) {
find(By.cssSelector("[data-testid='email-input']")).clear();
find(By.cssSelector("[data-testid='email-input']")).sendKeys(email);
}
@Step("Enter password")
public void enterPassword(String password) {
find(By.cssSelector("[data-testid='password-input']")).clear();
find(By.cssSelector("[data-testid='password-input']")).sendKeys(password);
}
@Step("Click the login button")
public void clickLoginButton() {
find(By.cssSelector("[data-testid='login-submit']")).click();
}
@Step("Verify user is on the dashboard")
public void verifyOnDashboard() {
waitForCondition()
.until(driver -> driver.getCurrentUrl().contains("/dashboard"));
assertThat(getDriver().getCurrentUrl()).contains("/dashboard");
}
@Step("Verify welcome message contains: {0}")
public void verifyWelcomeMessage(String text) {
String message = find(By.cssSelector("[data-testid='welcome-message']")).getText();
assertThat(message).contains(text);
}
@Step("Verify error message: {0}")
public void verifyErrorMessage(String expected) {
String actual = find(By.cssSelector("[data-testid='error-message']")).getText();
assertThat(actual).isEqualTo(expected);
}
}// src/test/java/features/api/UsersApiTest.java
package features.api;
import io.restassured.http.ContentType;
import net.serenitybdd.junit5.SerenityJUnit5Extension;
import net.serenitybdd.rest.SerenityRest;
import org.junit.jupiter.api.*;
import org.junit.jupiter.api.extension.ExtendWith;
import static net.serenitybdd.rest.SerenityRest.*;
import static org.hamcrest.Matchers.*;
@ExtendWith(SerenityJUnit5Extension.class)
@DisplayName("Users API")
class UsersApiTest {
private static final String BASE_URL = "http://localhost:3000/api";
@Test
@DisplayName("Should return list of users")
@Tag("api")
@Tag("smoke")
void shouldReturnUsers() {
given()
.baseUri(BASE_URL)
.contentType(ContentType.JSON)
.when()
.get("/users")
.then()
.statusCode(200)
.body("size()", greaterThan(0))
.body("[0].name", notNullValue())
.body("[0].email", notNullValue());
}
@Test
@DisplayName("Should create a new user")
@Tag("api")
void shouldCreateUser() {
String userJson = """
{
"name": "Alice Johnson",
"email": "[email protected]",
"role": "user"
}
""";
given()
.baseUri(BASE_URL)
.contentType(ContentType.JSON)
.body(userJson)
.when()
.post("/users")
.then()
.statusCode(201)
.body("name", equalTo("Alice Johnson"))
.body("email", equalTo("[email protected]"))
.body("id", notNullValue());
}
}# src/test/resources/serenity.conf
serenity {
project.name = "My Project Acceptance Tests"
test.root = "features"
take.screenshots = FOR_EACH_ACTION
browser.maximized = true
webdriver {
driver = chrome
autodownload = true
}
}
headless.mode = true
environments {
default {
webdriver.base.url = "http://localhost:3000"
}
staging {
webdriver.base.url = "https://staging.example.com"
}
production {
webdriver.base.url = "https://www.example.com"
}
}
chrome {
switches = "--headless;--no-sandbox;--disable-dev-shm-usage;--window-size=1920,1080"
}<!-- pom.xml (key dependencies) -->
<properties>
<serenity.version>4.1.0</serenity.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-webdriver</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>
<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-ensure</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.version}</version>
<executions>
<execution>
<id>serenity-reports</id>
<phase>post-integration-test</phase>
<goals>
<goal>aggregate</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>name: Serenity BDD Tests
on: [push, pull_request]
jobs:
serenity:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: 'temurin'
java-version: '17'
- name: Run tests and generate reports
run: mvn clean verify
env:
BASE_URL: http://localhost:3000
- uses: actions/upload-artifact@v4
if: always()
with:
name: serenity-reports
path: target/site/serenity/Target.the("login button") produces report entries like "Alice clicks on the login button".Login.asAdmin(), Login.withCredentials(email, pass) improve readability and reusability.FOR_EACH_ACTION in CI and FOR_FAILURES locally to balance report quality and speed.mvn test) to generate Serenity HTML reports. The verify phase triggers report aggregation.Target.the("").locatedBy("...") produces unreadable reports. Always provide descriptive Target names.@Step, actions do not appear in Serenity reports, losing the living documentation benefit.webdriver.base.url instead of hardcoded strings.mvn verify and archive reports.@BeforeEach or Serenity's automatic browser management.~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.