# Serenity BDD > Serenity BDD is an open-source Java library for writing acceptance and regression tests (web, REST and mobile) that produce rich living-documentation reports. Current release described here: 5.4.1 (the Playwright modules are published up to 5.3.11). Requires Java 17+. Source: https://github.com/serenity-bdd/serenity-core ## Notes for AI coding assistants Serenity supports several styles. All of them are current and valid; none is preferred or deprecated: - Page Objects with a step library (`@Steps`), using Selenium or Playwright - Screenplay, using Selenium, Playwright or REST - JUnit 5 or Cucumber as the test runner If the user hasn't said which style to use and the project doesn't show one (check `pom.xml` for `serenity-screenplay*`, `serenity-playwright`, `serenity-cucumber`, and look at existing tests), ask. Don't pick a default. Within a project, stay in one style, one driver and one runner. Don't mix Page Objects and Screenplay in one test. Packages that are often guessed wrongly (verified against 5.4.1): - `@Step`, `@Steps`, `@Managed`, `@DefaultUrl`, `@WithTag`: `net.serenitybdd.annotations` (not `net.thucydides.core.annotations`). - JUnit 5: `@ExtendWith(SerenityJUnit5Extension.class)` from `net.serenitybdd.junit5`. Don't use `@RunWith(SerenityRunner.class)` in a JUnit 5 project. - Page Objects: `net.serenitybdd.core.pages.PageObject` and `WebElementFacade`. `PageObject.open()` is final: don't redeclare it; use `@DefaultUrl` or `openAt(url)`. - Screenplay core: `Actor`, `Task`, `Question`, `Performable` in `net.serenitybdd.screenplay`; `OnStage` and `OnlineCast` in `net.serenitybdd.screenplay.actors`; `GivenWhenThen.seeThat` in `net.serenitybdd.screenplay`. - Screenplay with Selenium (`serenity-screenplay-webdriver`): `BrowseTheWeb` in `net.serenitybdd.screenplay.abilities`; `Open`, `Click`, `Enter`, `Hit` in `net.serenitybdd.screenplay.actions`; `Target` in `net.serenitybdd.screenplay.targets` (`locatedBy` takes a String that is read as XPath if it compiles as XPath, otherwise CSS: `.new-todo` is CSS but `input.new-todo` is read as XPath and finds nothing, so use `located(By.cssSelector(..))` for tag-prefixed CSS); questions such as `Text` and `Value` in `net.serenitybdd.screenplay.questions`. `Text.ofEach(..)` is a `Question>`. Web `Ensure` is `net.serenitybdd.screenplay.ensure.Ensure` (artifact `serenity-ensure`). - Screenplay with Playwright (`serenity-screenplay-playwright`): everything is under `net.serenitybdd.screenplay.playwright` (`abilities.BrowseTheWebWithPlaywright`, `actors.PlaywrightCast`, `interactions.*`, `questions.*`, `assertions.Ensure`). Locators are Playwright selector strings. `Text.ofEach(..)` returns `List`. - Screenplay REST (`serenity-screenplay-rest`): `net.serenitybdd.screenplay.rest.abilities.CallAnApi`, `...rest.interactions.{Get,Post,Ensure}`, `...rest.questions.LastResponse`. - Playwright Page Objects (`serenity-playwright`): plain classes holding a `com.microsoft.playwright.Page`, registered with `PlaywrightSerenity.registerPage(page)` from `net.serenitybdd.playwright`. They don't extend `PageObject`. - `ScenarioSteps` (`net.thucydides.core.steps`) and `Pages` (`net.thucydides.core.pages`) are correctly in `net.thucydides`. Many `net.thucydides` packages are current core packages, so don't treat that prefix as a sign of old code. - Cucumber with JUnit 5: a `@Suite` class with `@IncludeEngines("cucumber")` and the plugin `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel`. - Serenity 5.4.1 depends on JUnit 6.1.3 and Cucumber 8.0.4. Don't pin older JUnit or Cucumber versions in the pom: the code compiles but the tests fail at run time with "conflicting versions". Import the JUnit BOM (`org.junit:junit-bom`) to keep all JUnit artifacts aligned. - Configuration lives in `src/test/resources/serenity.conf`. `serenity.*` and `thucydides.*` keys are synonyms. Use only keys listed in the reference. Every page below is also available as plain markdown (the same address with `.md` appended, and all of them in one file at https://serenity-bdd.github.io/llms-full.txt). Start with the guide to writing tests, then read the page for the style in use. - How to write and check Serenity tests with an AI assistant (request, compile, run, can it fail, read the report): https://serenity-bdd.github.io/docs/ai/ai_writing_tests.md - Using Serenity with AI assistants (AGENTS.md files, agent skill, reviewer agent, what was measured): https://serenity-bdd.github.io/docs/ai/ai_using_assistants.md - Screenplay (Selenium, Playwright, REST, Cucumber): https://serenity-bdd.github.io/docs/ai/ai_screenplay_idioms.md - Page Objects (Selenium, Playwright): https://serenity-bdd.github.io/docs/ai/ai_page_object_idioms.md - REST with SerenityRest and a step library: https://serenity-bdd.github.io/docs/ai/ai_rest_step_library.md - Cucumber (runner, tags under Maven and Gradle, hooks, shared state, re-running failures, parallel): https://serenity-bdd.github.io/docs/ai/ai_cucumber.md - Configuration (serenity.conf, properties, environments, reading values; which source wins): https://serenity-bdd.github.io/docs/ai/ai_configuration.md - Living documentation (how titles, wording and structure become the report): https://serenity-bdd.github.io/docs/ai/ai_living_documentation.md - Reviewing tests and the report (checklist): https://serenity-bdd.github.io/docs/ai/ai_review_checklist.md - Upgrading from Serenity 2.x, 3.x or 4.x, and recognising old code (what moved, what to change; tested on real projects): https://serenity-bdd.github.io/docs/ai/ai_upgrading.md - Reading test results from files, also after a failed build (JSON outcome files, script, JSON summary): https://serenity-bdd.github.io/docs/ai/ai_test_results.md - Dependencies and configuration: https://serenity-bdd.github.io/docs/ai/ai_project_setup.md - Troubleshooting by error message (build, report and browser problems, each reproduced and fixed): https://serenity-bdd.github.io/docs/ai/ai_troubleshooting.md Check that generated code compiles (`mvn test-compile`) before running it. Then run `mvn verify`, check that the test fails when the behaviour is broken, and read the report in `target/site/serenity/index.html`. --- # Using Serenity BDD with AI Coding Assistants AI coding assistants write Serenity tests quickly, and they most often go wrong by guessing: a package name from an old release, a method that does not exist, or a mix of two ways of working. Everything on this page is there to stop that. All of it describes the current release (Serenity 5.4.1; the Playwright modules are published up to 5.3.11), and every claim in it was checked against the released jars or by running it. Serenity supports several ways of writing tests, and all of them are current: Page Objects with step libraries, Screenplay, Cucumber (with Screenplay or with page objects), REST with a step library or with Screenplay, with Selenium or Playwright, with JUnit 5 or Cucumber. None is recommended over another. An assistant that has these materials asks which one you want when it cannot tell from the project, instead of choosing for you. ## What is available | Material | What it is | Where | |---|---|---| | Documentation pages | Exact packages, dependencies, idioms and compiling examples for each style, plus configuration, upgrading, living documentation and troubleshooting by symptom | [Idioms and Imports by Style](ai_idioms_overview) | | Machine-readable results | The JSON outcome files Serenity writes for every test (also after a failed build), a script that summarises a run, and the optional JSON summary report | [Reading Test Results Without the HTML Report](ai_test_results) | | `llms.txt` and `llms-full.txt` | An index of the documentation for AI assistants (this section first), and the full text of the pages. Every page is also plain markdown at its own address with `.md` appended, for example `https://serenity-bdd.github.io/docs/ai/ai_writing_tests.md` | [llms.txt](https://serenity-bdd.github.io/llms.txt), [llms-full.txt](https://serenity-bdd.github.io/llms-full.txt) | | `AGENTS.md` files | One per style. They record the style, driver and runner your project uses, so an assistant stays in it | [`docs/ai/agents-md/variants`](https://github.com/serenity-bdd/serenity-core/tree/main/docs/ai/agents-md/variants) in the serenity-core repository | | An agent skill (Claude Code) | The same material as the pages, loaded when you ask for Serenity tests: it chooses the style (or asks), writes the tests, compiles them, and reviews the report | [`docs/ai/skill/serenity-bdd`](https://github.com/serenity-bdd/serenity-core/tree/main/docs/ai/skill/serenity-bdd) | | A test-review agent (Claude Code) | A read-only agent that judges the new tests and the report they produce: intent, business wording, grouping | [`docs/ai/skill/agents`](https://github.com/serenity-bdd/serenity-core/tree/main/docs/ai/skill/agents) | ## Installing **Tell your assistant about the pages.** The simplest step needs no installation: give it the address of [llms.txt](https://serenity-bdd.github.io/llms.txt), or the page for your style, and ask it to compile what it writes (`mvn test-compile`). **Add an `AGENTS.md`.** Copy the file for your style from the variants folder to the root of your project and name it `AGENTS.md`. The names are: | File | Style | |---|---| | `AGENTS.po-selenium.md`, `AGENTS.po-playwright.md` | Page Objects with a step library (Selenium, Playwright) | | `AGENTS.sp-selenium.md`, `AGENTS.sp-playwright.md`, `AGENTS.sp-rest.md` | Screenplay (Selenium, Playwright, REST) | | `AGENTS.cuke-sp.md`, `AGENTS.cuke-pages.md` | Cucumber with Screenplay, Cucumber with page objects | | `AGENTS.rest-steps.md` | REST with a `SerenityRest` step library | Assistants that read `AGENTS.md` pick it up automatically. Claude Code reads `CLAUDE.md`, so copy the same file to `CLAUDE.md` as well. **Install the skill (Claude Code).** Copy the folder `docs/ai/skill/serenity-bdd` from the serenity-core repository to `.claude/skills/serenity-bdd` in your project, or to `~/.claude/skills/serenity-bdd` to have it in every project. To add the reviewer, copy `docs/ai/skill/agents/serenity-test-reviewer.md` to `.claude/agents/` (or `~/.claude/agents/`). The skill loads when you ask for Serenity tests. If your project does not show which style it uses and you did not say, it asks you before writing anything. ## What was measured Models were given test-writing tasks against a small online bookshop with known, planted defects. A generated test counted as good only if it compiled, passed on the healthy application, and failed when the application had the defect it was meant to catch. Each figure below is for three Claude models, with and without the material. | Test | Without | With the references | |---|---|---| | 14 new tasks, Page Objects and Screenplay, one-shot | 62% | 81% | | 8 new Playwright tasks, one-shot | 67% | 96% | | 8 new REST and Cucumber tasks (written after the guidance), one-shot | 36% | 99% | | Same tasks with tools (the model can run Maven and fix its own errors) | at or near 100% | at or near 100%, with about half as many Maven runs | So the main gain is in code written in one go, and in fewer retries when the model can run the build. The materials were also scored for how well the generated tests work as living documentation (clear scenario titles in business language, tests grouped by function): the score went from about 70% to about 90%. These are results from one evaluation, run by the authors of the materials, on Claude models and Claude Code. The full method and results are in the [evaluation write-up](https://github.com/serenity-bdd/serenity-core/blob/main/docs/ai/baseline-results.md) in the serenity-core repository, including what was not tested. ## Known limits - The pages and the skill are verified against Serenity 5.4.1. Behaviour fixed in later releases is noted where it matters. - The skill and reviewer agent are for Claude Code. The pages, `llms.txt` and the `AGENTS.md` files are plain text and work with any assistant, but they were only measured with Claude models. - Mobile testing, Spring and Guice object factories, and cloud grid configuration are not covered. - If a page here is wrong, or an error message you hit is missing from [troubleshooting](ai_troubleshooting), please open an issue. --- # Writing Serenity BDD Tests with an AI Assistant This page is a workflow for getting good Serenity tests from an AI coding assistant, and for checking them. It works with any assistant. Where it mentions the agent skill, that is the Claude Code skill described in [Using Serenity BDD with AI Coding Assistants](ai_using_assistants). The short version: **say which style you want, make the assistant compile and run what it writes, and read the report yourself.** An assistant that has not been told the style will pick one, and a test that compiles and passes can still check the wrong thing. ## 1. Set up once Do whichever of these you can; each helps on its own. - Put an `AGENTS.md` for your style in the project root (and a copy named `CLAUDE.md` for Claude Code). It records the style, driver and runner, so every later request stays in them. See [Installing](ai_using_assistants#installing). - Or install the agent skill, or point the assistant at [llms.txt](https://serenity-bdd.github.io/llms.txt) or at the page for your style under [Idioms and Imports by Style](ai_idioms_overview). - Make sure the project already builds and has a working Serenity setup ([Project Setup](ai_project_setup)). An assistant extends a working project far better than it creates one from nothing. ## 2. Write the request A good request names five things. If one is missing, a well-configured assistant asks you; a poorly configured one guesses. | Say | Why | Example | |---|---|---| | The style | Serenity supports several, all current; the assistant must not mix them | "Serenity Screenplay", "Page Objects with a step library", "Cucumber with page objects" | | The driver | Selenium and Playwright use different packages and assertions | "with Selenium", "with Playwright", "REST only" | | The runner | JUnit 5 and Cucumber are set up differently | "JUnit 5", "Cucumber on the JUnit Platform" | | The behaviour, in business terms | The scenario title and step wording become the report | "a reader signs in with a wrong password and sees 'Invalid email or password'" | | The expected values, exactly | The assertion should check the specific value, not that something is shown | "the total is $80.75", "an order number of the form ORD- followed by digits" | Also useful: how the application is found (base URL, how to start it), the accounts or test data to use, and anything it must not touch. If you are not sure which style to choose, say so. The skill and the `AGENTS.md` files are written to make the assistant list the options and ask, not to choose for you. ## 3. Make it compile and run Ask the assistant to run the build itself and fix what fails: ``` mvn test-compile # catches wrong package names and methods quickly mvn verify # runs the tests and builds the report ``` Add `-Dheadless.mode=true` to run a browser test without opening a window. Compile errors from wrong imports are the most common fault in generated Serenity code, and the second pass fixes them. An assistant that can run Maven usually gets there; one that cannot (a chat window) will often need you to paste back the error. If the build fails, there is no HTML report, but the result of every test is still in `target/site/serenity/*.json`; ask the assistant to read it (see [Reading Test Results Without the HTML Report](ai_test_results)). Do not stop at "it compiles". A test that passes on the application as it is has proved little until you know it can fail. ## 4. Check that the test can fail Break the application on purpose, or point the test at a state where the behaviour is wrong, and run it again. If it still passes, it is not checking what you asked for. This is the check the evaluation used: a generated test counted as good only if it passed on the healthy application and failed on the application with the matching defect. Common causes of a test that cannot fail: - the assertion is on a page title or heading that is present either way; - it checks that an element exists, not what it says; - it asserts the right value against the wrong element; - it waits with a sleep and checks nothing afterwards. ## 5. Read the report Open `target/site/serenity/index.html` after `mvn verify`. Read it as someone who has not seen the code: - Do the scenario titles say what the business behaviour is? - Do the step names read as a story ("Reader adds 'Clean Code' to the basket"), with no selectors, ids, URLs or status codes? - Are the tests grouped by function (packages, or `@Narrative` and readme files), so the requirements view has a sensible shape? - Does each test end with the expectation you asked for? The [review checklist](ai_review_checklist) has the full list, and [Living Documentation](ai_living_documentation) explains how titles and structure become the report. In Claude Code, the test-review agent applies the same checklist to the new tests and the report. ## 6. Ask for changes, one at a time When something is wrong, say what you saw in the report and what you want instead ("the step says `Click.on(#add)`; it should say what the reader does"). Keep to one style and one driver as you iterate. ## A worked example This is a real run, not a mock-up. Claude Opus was asked, through Claude Code with the skill installed, to write a test against a small online bookshop that the authors keep for evaluation. The application has a catalog, a basket and a checkout. The prompt (shortened here) named the style, the driver and the runner, the behaviour, and the exact expected value: > Using Serenity BDD Screenplay with Selenium and JUnit 5, write a test where a reader signs in, adds The Pragmatic Programmer, checks out as Ada Lovelace at '1 Main St' in Germany with the terms accepted, and checks that the confirmation shows an order number of the form ORD- followed by digits. It was also given a description of the pages (element ids and labels), as you would give for your own application. The assistant wrote the test and its tasks, ran `mvn verify -Dheadless.mode=true`, and reported one test, no failures. The test: ```java @ExtendWith(SerenityJUnit5Extension.class) class PlaceOrderTest { @Managed(driver = "chrome") WebDriver browser; @BeforeEach void setTheStage() { OnStage.setTheStage(new OnlineCast()); } @Test void readerCanOrderABookAndSeeTheOrderNumber() { Actor reader = OnStage.theActorCalled("Reader"); reader.attemptsTo( SignIn.withCredentials("reader@example.com", "correct-horse"), AddToBasket.theBook("The Pragmatic Programmer"), Checkout.as("Ada Lovelace", "1 Main St", "Germany"), WaitUntil.the(CheckoutPage.CONFIRMATION, isVisible())); reader.should(seeThat("the order confirmation", Text.of(CheckoutPage.CONFIRMATION), matchesPattern(".*\\bORD-\\d+\\b.*"))); } } ``` and one of the tasks it created, with the business wording in the step title: ```java public class AddToBasket { public static Performable theBook(String title) { return Task.where("{0} adds '" + title + "' to the basket", Click.on(CatalogPage.ADD_TO_BASKET_FOR.of(title))); } } ``` **Step 4, can it fail?** The evaluation then ran the same test against a copy of the bookshop in which the confirmation no longer contains an order number. The test failed, with the message `Expected: an element containing 'ORD-' but: ... [Thank you! Order]`. So the assertion does check the order number, and the test was counted as good. **Step 5, a review.** Things a reader would still want to change: the sign-in task opens a hard-coded `http://localhost:8765/login`, which belongs in `webdriver.base.url` in `serenity.conf` (see [Configuration](ai_configuration)); and the scenario title is a method name, `readerCanOrderABookAndSeeTheOrderNumber`, which the report turns into a readable title, but you may prefer a more specific one. These are the kind of follow-ups to ask for in step 6. Passing the evaluation did not mean there was nothing to improve. ## What to expect In the evaluation (Claude models, a small test application; full figures on [Using Serenity BDD with AI Coding Assistants](ai_using_assistants#what-was-measured)): - Written in one go, without the reference pages, a large share of tests failed: usually an import that does not exist, or a method from a different style. With the pages or the skill, most compiled and passed. - With tools, so the assistant can run Maven and fix its own errors, nearly every test ended up good with or without the materials. What the materials changed was the number of Maven runs, about half. - Passing is not the same as good: the checks above (can it fail, does the report read well) are still yours to do. ## Common mistakes to watch for | In the generated code | What it means | |---|---| | Imports from `net.serenitybdd.screenplay.*` in a Page Objects project, or `@Steps` in a Screenplay project | Two styles mixed; ask for one | | `Thread.sleep(...)` | A fixed wait; Serenity waits for elements itself, or use `WaitUntil` (Screenplay) | | Selectors, ids or URLs in step titles | The report will read as a script, not a story | | A selector like `$("button")` or `Target.the("x").locatedBy("input.x")` | A string that starts with a tag name is read as XPath, not CSS, so the element is not found at run time. Use `#id`, `.class` or `By.cssSelector(...)`; see [Page Objects](ai_page_object_idioms) and [Screenplay](ai_screenplay_idioms) | | A numeric REST assertion like `equalTo(90.0)` that fails | REST-assured returns JSON decimals as `Float`; see [REST](ai_rest_step_library) | | `@Managed WebDriver` in a step library or other helper class | It is filled in only in the test class, so the field is null elsewhere; extend `UIInteractionSteps` and use `getDriver()`, or use a page object. See [Troubleshooting](ai_troubleshooting) | | A Cucumber step that is undefined or shown as pending | The step definition does not match the step text, and an undefined step fails the build; check the Cucumber expression. See [Cucumber](ai_cucumber) | | An import such as `net.thucydides.core.annotations.Step` that does not compile | The annotations moved to `net.serenitybdd.annotations`; see [Upgrading](ai_upgrading). Many other `net.thucydides` packages are still current core packages, so judge each import against the pages | The [troubleshooting page](ai_troubleshooting) lists build, report and browser errors by the message you see, with a fix that was reproduced. --- # Idioms and Imports by Style These pages list the exact packages, dependencies and idioms for each supported way of writing tests with Serenity BDD, with compiling examples. They are written for people and for AI coding assistants, which most often go wrong by guessing a package name. Serenity supports several styles. All of them are current and valid: | Approach | Drivers | Page | |---|---|---| | Page Objects with a step library | Selenium, Playwright | [Page Objects: Imports and Idioms](ai_page_object_idioms) | | Screenplay | Selenium, Playwright, REST | [Screenplay: Imports and Idioms](ai_screenplay_idioms) | | Cucumber with Screenplay | Selenium | [Screenplay: Imports and Idioms](ai_screenplay_idioms) | | Cucumber with page objects and a step library | Selenium | [Cucumber](ai_cucumber) | | REST with a step library (no Screenplay) | REST-assured via SerenityRest | [REST with SerenityRest and a Step Library](ai_rest_step_library) | Use JUnit 5 as the runner for all of them. Dependencies, the report plugin and `serenity.conf` are on [Project Setup](ai_project_setup). ## Choosing a style Pick the style the project already uses, and stay in it: one approach, one driver and one runner per project, and don't mix Page Objects and Screenplay in one test. To see which a project uses, look at `pom.xml` (`serenity-screenplay*`, `serenity-playwright`, `serenity-cucumber`) and the existing tests. For a new project, choose by what suits the team. None of the styles is deprecated. ## If you use an AI coding assistant See [Using Serenity BDD with AI Coding Assistants](ai_using_assistants) and [Writing Serenity BDD Tests with an AI Assistant](ai_writing_tests) for the `AGENTS.md` files, the agent skill and `llms.txt`. In short: tell it which style, driver and runner you want, or point it at these pages (or at [llms.txt](https://serenity-bdd.github.io/llms.txt)). Ask it to compile its output (`mvn test-compile`) before running it. Many `net.thucydides` packages are current core packages, so that prefix on its own is not a sign of outdated code: judge each class against these pages. --- # Living Documentation: Intent, Wording and Structure Verified against Serenity **5.4.1**. The behaviour described here was run against the example at the end of the page. ## The idea A Serenity report is read by people who will never open the code: product owners, analysts, new team members, auditors. Its Requirements tab is a description of what the application does, built from the tests. So every scenario has two jobs: check the application, and tell a reader **what the business expects and why it matters**. A test that only passes is half done. Write for that reader: - The **scenario title** says the business rule or outcome, as a sentence a user would recognise. - The **steps** read as a story in business language: who does what, in what situation, and what they should see. - The **tests are grouped by what the application does** (ordering, searching, signing in), so the report is a navigable table of contents. ## 1. Scenario titles: the rule, not the mechanics | Poor | Good | |---|---| | `testCheckout1` | `A reader sees an order number after paying` | | `loginTest` | `Signing in with a wrong password is refused with a clear message` | | `shouldReturn201` | `Placing an order confirms it with an order number` | | `searchTest` | `Searching by title finds only matching books` | - **JUnit 5:** use `@DisplayName("...")` on the method; the report title is exactly that (verified). Without it, Serenity turns the method name into words (`shouldKeepEveryBookAddedToTheBasket` becomes "Should keep every book added to the basket"), so a method name written as a sentence is also fine. A `test` prefix or a number is not. - **Cucumber:** the `Scenario:` name is the title; write it as a sentence. Say the rule being shown, not the clicks. - Name what the user gets or what the rule is. Avoid HTTP codes, element ids, class names and the word "test" in titles. ## 2. Steps: business language, one level above the mechanics The report lists the steps of each scenario. Make them read like the scenario narrated. - **Screenplay:** give every `Task` a sentence with `{0}` for the actor, and put the important data in it (verified): ```java Task.where("{0} adds '" + title + "' to the basket", ...) // report: Ada adds 'Refactoring' to the basket ``` Tasks are business actions (`SignIn.withCredentials`, `Checkout.as`). Interactions (`Click`, `Enter`) stay inside tasks, so the report shows the business level first. Questions and `seeThat` checks read as expectations: `seeThat("the order confirmation", TheConfirmation.text(), containsString("ORD-"))`. - **Page Objects:** annotate step-library methods with `@Step("Search the catalog for '{0}'")` (verified; `{0}` is the first parameter). Put the business verb in the step method (`shopper.searchesFor(term)`), not in the test (`driver.findElement(..)`). - **Cucumber:** the Gherkin is the documentation. Use Given/When/Then for context, action, outcome. Keep each step about the business ("When Ada places the order"), not the UI ("When I click the button with id place-order"). Use a `Background` for context shared by all scenarios, and a `Scenario Outline` with an `Examples` table for the same rule over several values, with a meaningful column per input. - Do not put CSS selectors, XPath, URLs, element ids, sleeps or HTTP status codes in step text. They belong inside the page object, task or step definition. - Serenity writes the lines under a task itself: `Open.url(..)` shows as "opens the browser at http://..." and `Enter`/`Click` show the element names. You can't remove them, so keep URLs in one place and give `Target`s business names (`Target.the("email field")`). The task line above them is what the reader reads first. - The last step of a scenario states the business expectation ("the confirmation shows an order number"), not a technical detail. ## 3. Organise by function The report's Requirements tab is a tree. Serenity builds it from how you organise the tests: | Style | How the tree is built | Verified result | |---|---|---| | JUnit 5 (any style), no annotations | the **package** path of the test class | `shop.ordering.WhenAReaderBuildsABasketTest` gives `Shop (capability)` > `Ordering (feature)` > `When a reader builds a basket test (feature)` | | JUnit 5, annotations (recent releases; see the "Annotation-Based Requirements" docs page) | `@Epic` > `@Feature` > `@Story` on the test class | `@Epic("Buying books") @Feature("Paying for an order") @Story("Pay by card")` gives `Buying books (capability)` > `Paying for an order (feature)` > `Pay by card (story)` | | Cucumber | the **folders** under `src/test/resources/features`, and the feature file | `features/shopping/basket.feature` gives `Shopping (capability)` > `Shopping basket (feature)` | Rules that follow: 1. **Group by capability, then feature.** For example `ordering`, `browsing`, `accounts` as packages (or folders), with one test class (or feature file) per feature inside, each holding the scenarios of that feature. Not one class called `ShopTest` with everything in it, and not one package per technical layer. 2. **Prefer one way for the whole project.** If package-based and annotated classes are mixed, the Requirements page shows both as separate top-level trees side by side (verified), and each class is found in only one of them, so the project reads as two organisations. Follow what is already there. 3. **Class names end up in the tree.** With packages, the class name becomes the feature name, `Test` suffix included (`WhenSearchingTheCatalogTest` is shown as "When searching the catalog test", seen in a real report). Put `@DisplayName("Searching the catalog")` on the class: the feature then shows that text (verified). With annotations, the `@Feature` and `@Story` text is what the reader sees. Write these as business phrases. 4. **Cucumber:** one feature file per feature, in a folder per capability; the text under `Feature:` (a paragraph of business context before the first scenario) is shown on the feature page (verified). ## 4. Descriptions of the areas - **Cucumber:** a `readme.md` in a features folder is the description of that capability. Its first line should be the name (`## Shopping`). Verified: it appears in the report. - **JUnit 5:** put `readme.md` files under `src/test/resources/stories//` (for the example `src/test/resources/stories/shop/ordering/readme.md`); the text appears in the report. The same file in `src/test/resources/shop/ordering/` was **ignored** (verified). - **`@Narrative(text = {...})` did not appear in the JUnit 5 report** in 5.4.1 (verified, both on the class and in `package-info.java`). Don't rely on it; use the `readme.md` files. Keep a description to a few sentences: what the area is for, and the rules that matter. ## 5. What a good scenario looks like The shape only: `SignIn`, `Checkout`, `shopper` and the rest are the project's own classes. Page Objects + step library: ```java @Test @DisplayName("Searching by title finds only matching books") void searchingByTitle() { shopper.searchesFor("Refactoring"); shopper.shouldSeeOnlyBooksCalled("Refactoring"); } ``` Screenplay: ```java @Test @DisplayName("A reader sees an order number after paying") void orderNumber() { ada.attemptsTo( SignIn.withCredentials(reader), AddToBasket.theBook("The Pragmatic Programmer"), Checkout.as("Ada Lovelace").inCountry("Germany")); ada.should(seeThat("the order confirmation", TheConfirmation.text(), matchesPattern("Thank you! Order ORD-\\d+"))); } ``` Cucumber: ```gherkin Feature: Placing an order Readers pay for the books in their basket and get an order number back. Scenario: A reader sees an order number after paying Given Ada has "The Pragmatic Programmer" in her basket When she checks out and accepts the terms Then she sees an order number like ORD-1001 ``` ## 6. Checking it After the tests run, the report is what the reader sees. Run `scripts/outline.py /target/site/serenity` (from the skill directory): it prints the requirements tree, the scenario titles and the steps of each scenario, and flags the mechanical problems (titles that look like method names, selectors or ids in step text, ungrouped tests). Read its output as the reader would: could someone who has never seen the code say what the application does? The `serenity-test-reviewer` agent does this check and the judgement part (`review-checklist.md`). ## 7. Not covered Pending and manual scenarios, tags and `@WithTag`, Serenity's custom requirement types (`serenity.requirement.types`), the Jira requirements provider, `@Title`, and the exact look of the HTML report. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `scripts/outline.py` ```python #!/usr/bin/env python3 """Print the living documentation of a Serenity run the way a reader sees it, and flag mechanical problems. usage: outline.py [--json] (the directory is normally /target/site/serenity; only the *.json outcome files are read) Prints: the requirements path of each test, its title, and its steps. Then a list of FLAGS for things a reader would trip on. The flags are objective checks; whether the wording is good is for the reviewer to judge. """ import glob, json, os, re, sys SELECTOR = re.compile(r"(#[\w-]+|\.[a-z][\w-]+\b.*[>\[]|//\w|\[@|By\.\w+|css=|xpath|data-testid|getByRole|\bid=)", re.I) URL = re.compile(r"https?://|localhost|:\d{4}\b") HTTP = re.compile(r"\b(?:status(?: code)?|HTTP)\s*[:=]?\s*[1-5]\d\d\b|\b(?:200|201|204|400|401|403|404|422|500)\b") PLUMBING = re.compile(r"\b(sleep|wait(?:s)? \d|findElement|executeScript|sendKeys|driver\.|click(?:s)? (?:on )?(?:the )?(?:element|button with|link with)|locator)\b", re.I) def load(d): rows = [] for f in sorted(glob.glob(os.path.join(d, "*.json"))): try: j = json.load(open(f)) except ValueError: continue if not isinstance(j, dict) or "userStory" not in j: continue steps = [] def walk(nodes, depth): for s in nodes or []: steps.append((depth, s.get("description", ""))) walk(s.get("children"), depth + 1) walk(j.get("testSteps"), 0) us = j.get("userStory", {}) rows.append(dict(path=us.get("path", ""), story=us.get("storyName", ""), title=j.get("title") or j.get("name", ""), result=j.get("result", ""), steps=steps, description=j.get("description") or "")) return rows def flags(rows): out = [] if not rows: return ["no outcome files found: run the tests first"] paths = {r["path"] for r in rows} stories = {r["story"] for r in rows} if len(rows) >= 4 and len(stories) == 1: out.append("all %d scenarios are in one group (%s): split them by feature" % (len(rows), next(iter(stories)))) depths = {len(re.split(r"[/.]", p)) for p in paths if p} if len(rows) >= 6 and max(depths or {0}) < 2: out.append("no functional hierarchy: use packages/folders or @Epic/@Feature/@Story to group the scenarios") seen = {} for r in rows: seen.setdefault(r["title"].lower(), []).append(r["path"]) for t, ps in seen.items(): if len(ps) > 1: out.append("duplicate scenario title '%s' (%d times)" % (t, len(ps))) for st in sorted(stories): if re.search(r"[a-z]Test$|Tests$|^Test[A-Z]", st or ""): out.append("feature '%s' shows with its class name (\"%s\"): put @DisplayName with a business phrase on the class" % (st, re.sub(r"(? ") or "(no group)") for r in rs: print(" - %s [%s]" % (r["title"], r["result"])) for depth, d in r["steps"]: print(" %s%s" % (" " * depth, d)) print("\nFLAGS (%d)" % len(fl)) for f in fl: print(" ! " + f) sys.exit(1 if fl else 0) if __name__ == "__main__": main() ``` ### `living-docs/shop/browsing/WhenSearchingTheCatalogTest.java` ```java package shop.browsing; import net.serenitybdd.annotations.Step; import net.serenitybdd.annotations.Steps; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Searching the catalog") class WhenSearchingTheCatalogTest { @Steps ShopperSteps shopper; @Test void searchingByTitleFindsTheMatchingBook() { shopper.searchesFor("Refactoring"); shopper.shouldSeeBooks(1); } public static class ShopperSteps { private int found; @Step("Search the catalog for '{0}'") public void searchesFor(String term) { found = "Refactoring".equals(term) ? 1 : 0; } @Step("The catalog lists {0} book(s)") public void shouldSeeBooks(int expected) { assertThat(found).isEqualTo(expected); } } } ``` ### `living-docs/shop/ordering/AddToBasket.java` ```java package shop.ordering; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; public class AddToBasket { public static Performable theBook(String title) { return Task.where("{0} adds '" + title + "' to the basket", actor -> actor.recall("basket").add(title)); } } ``` ### `living-docs/shop/ordering/Basket.java` ```java package shop.ordering; import java.util.ArrayList; import java.util.List; /** A tiny in-memory basket so the example needs no browser or server. */ public class Basket { private final List titles = new ArrayList<>(); public void add(String title) { titles.add(title); } public int size() { return titles.size(); } public boolean isEmpty() { return titles.isEmpty(); } } ``` ### `living-docs/shop/ordering/WhenAReaderBuildsABasketTest.java` ```java package shop.ordering; import net.serenitybdd.annotations.Narrative; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.actors.OnlineCast; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Building a basket") @Narrative(text = {"As a reader", "I want to collect books in a basket", "So that I can buy them together"}) class WhenAReaderBuildsABasketTest { @BeforeEach void setTheStage() { OnStage.setTheStage(new OnlineCast()); } @Test @DisplayName("A reader can add a book to an empty basket") void aReaderCanAddABookToAnEmptyBasket() { Actor ada = OnStage.theActorCalled("Ada"); ada.remember("basket", new Basket()); ada.attemptsTo(AddToBasket.theBook("The Pragmatic Programmer")); assertThat(ada.recall("basket").size()).isEqualTo(1); } @Test void shouldKeepEveryBookAddedToTheBasket() { Actor ada = OnStage.theActorCalled("Ada"); ada.remember("basket", new Basket()); ada.attemptsTo(AddToBasket.theBook("Refactoring"), AddToBasket.theBook("Fluent Python")); assertThat(ada.recall("basket").size()).isEqualTo(2); } } ``` ### `living-docs-annotated/shop/annotated/WhenPayingForAnOrderTest.java` ```java package shop.annotated; import net.serenitybdd.annotations.Epic; import net.serenitybdd.annotations.Feature; import net.serenitybdd.annotations.Story; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) @Epic("Buying books") @Feature("Paying for an order") @Story("Pay by card") class WhenPayingForAnOrderTest { @Test void aReaderCanPayByCard() { } } ``` --- # Screenplay: Imports and Idioms Verified against Serenity **5.4.1** (Playwright modules: **5.3.11**, the latest published). The examples at the end of this page compile and their tests pass. Screenplay works with Selenium, Playwright and REST, with JUnit 5 or Cucumber. All of these are valid. Pick the one the user asked for or the project already uses, and don't mix them in one project. ## 1. Where things live The packages are not guessable. Guessing them is the most common cause of compile errors. ### Core (all Screenplay projects), artifact `serenity-screenplay` | Class | Package | |---|---| | `Actor`, `Task`, `Performable`, `Question`, `Interaction`, `Ability`, `Consequence`, `Tasks`, `RememberThat` | `net.serenitybdd.screenplay` | | `GivenWhenThen` (`seeThat`) | `net.serenitybdd.screenplay` (use as a static import) | | `OnStage`, `OnlineCast`, `Cast`, `Stage` | `net.serenitybdd.screenplay.actors` | | `CastMember` | `net.serenitybdd.screenplay.annotations` | | `Check` (conditional tasks) | `net.serenitybdd.screenplay.conditions` | | `Wait`, `WaitUntil` | `net.serenitybdd.screenplay.waits` | ### Selenium, artifact `serenity-screenplay-webdriver` | Class | Package | |---|---| | `BrowseTheWeb` | `net.serenitybdd.screenplay.abilities` | | `Open`, `Click`, `Enter`, `Hit`, `Clear`, `Hover`, `Scroll`, `SelectFromOptions` | `net.serenitybdd.screenplay.actions` | | `Target` | `net.serenitybdd.screenplay.targets`. `Target.the("name").locatedBy("..")` takes a **String**. Serenity treats the string as XPath if it compiles as an XPath expression, otherwise as CSS. So `.new-todo`, `#id`, `[data-test=x]` and `li label` are CSS, but a tag-prefixed selector such as `input.new-todo`, `button` or `h1` is read as XPath and finds nothing. For those use `Target.the("name").located(By.cssSelector("input.new-todo"))` (or a leading `.`/`#`, or an explicit `//input[...]`). | | `Text`, `TextContent`, `Value`, `Attribute`, `Visibility`, `Presence`, `Enabled`, `Disabled`, `SelectedValue`, `CurrentVisibility`, `WebElementQuestion` | `net.serenitybdd.screenplay.questions` | `Ensure` for web is in artifact `serenity-ensure`: **`net.serenitybdd.screenplay.ensure.Ensure`**. ### Playwright, artifact `serenity-screenplay-playwright` Everything lives under `net.serenitybdd.screenplay.playwright`: | Class | Package | |---|---| | `BrowseTheWebWithPlaywright` | `...playwright.abilities` | | `PlaywrightCast` (use instead of `OnlineCast`) | `...playwright.actors` | | `Open`, `Click`, `Enter`, `Press`, `Navigate`, `Check`, `Hover`, `WaitFor` ... | `...playwright.interactions` | | `Text`, `Visibility`, `Count`, `Attribute` ... | `...playwright.questions` | | `Ensure` | `...playwright.assertions` | | `Target` | `...playwright.Target` | Playwright locators are plain Playwright selector strings (`".new-todo"`, `"text='Delete'"`) or a Playwright `Target`. Playwright classes that exist (all take a selector string or a `Target`): | Kind | Classes and entry points | |---|---| | Interactions (`...playwright.interactions`) | `Open.url(..)`, `Navigate`, `Click.on(..)`, `DoubleClick`, `RightClick`, `Hover.over(..)`, `Enter.theValue(..).into(..)`, `Press.keys("Enter")`, `Clear`, `Focus`, `Check.the(..)`, `Uncheck.checkbox(..)`, `SelectFromOptions.byVisibleText(..)` / `.byValue(..)` / `.byIndex(..)` then `.from(selector)`, `Upload`, `Drag`, `ScrollIntoView`, `WaitFor.selector(..)` | | Questions (`...playwright.questions`) | `Text.of(..)`, `Text.ofEach(..)` (`List`), `Value.of(..)`, `SelectedOptionText.of(..)`, `Checked.of(..)`, `Visibility.of(..)`, `Presence`, `Absence`, `Count.of(..)`, `Attribute`, `Enabled`, `Disabled`, `AllOptionTexts` | | Assertions (`...playwright.assertions.Ensure`) | `Ensure.that(selector).isVisible() / isHidden() / hasText(..) / containsText(..) / hasValue(..) / isChecked() / hasCount(n) / hasAttribute(n, v)` | **Don't use `Ensure.thatTheCurrentUrl()` or `Ensure.thatThePageTitle()`: they return package-private classes, and any call on them fails to compile** ("UrlEnsure.contains(java.lang.String) is defined in an inaccessible class or interface"; run in the verification script in the serenity-core repository (`docs/ai/skill`)). To check the URL, assert on the page: `assertThat(BrowseTheWebWithPlaywright.as(actor).getCurrentPage().url()).contains("/catalog")` (after `attemptsTo(..)`, in the test body), or better, check something the user sees on the new page (`Ensure.that("h1").hasText("Catalog")`). **A selector must match exactly one element** for `WaitFor.selector`, `Click.on`, `Enter`, `Ensure.that(..).hasText(..)` and `Text.of(..)` (Playwright's strict mode). A selector that matches a list (`"#books .book"` with five rows) fails at run time with `strict mode violation: ... resolved to 5 elements`. For a list: wait on the first row (`"#books .book >> nth=0"`), count with `Ensure.that("#books .book").hasCount(5)`, read all texts with `Text.ofEach(..)`, pick one with `">> nth=0"` or `">> nth=-1"` (last), or narrow with `has-text`/`text-is` (`"tr.book:has(td.title:text-is('Refactoring'))"`). `sp-playwright/todo/ListTest.java` runs these. **Don't build a Playwright text pattern with `Pattern.quote(..)`.** Playwright sends the pattern to the browser's JavaScript engine, which doesn't understand Java's `\Q..\E`, so `setHasText(Pattern.compile("^\\s*" + Pattern.quote(title) + "\\s*$"))` matches nothing and the step times out after 30 seconds (two Opus runs). Use the plain string (`setHasText(title)` matches by substring, ignoring case; `text-is('..')` / `has-text('..')` in a selector string match exactly or by substring), or write the pattern by hand (`Pattern.compile("^\\s*Clean Code\\s*$")`). Run in the verification script in the serenity-core repository (`docs/ai/skill`). In a Playwright selector string a role needs the prefix: `"role=heading[name=\"Sign in\"]"`; without `role=` it throws `InvalidSelectorError` (run in the verification script in the serenity-core repository (`docs/ai/skill`)). Playwright has no `net.serenitybdd.screenplay.ensure.Ensure` and no `Target` from `net.serenitybdd.screenplay.targets`: use the Playwright ones. See `sp-playwright/todo/FormTest.java`. ### REST, artifact `serenity-screenplay-rest` | Class | Package | |---|---| | `CallAnApi` | `net.serenitybdd.screenplay.rest.abilities` | | `Get`, `Post`, `Delete`, `Ensure` ... | `net.serenitybdd.screenplay.rest.interactions` | | `LastResponse`, `TheResponse` | `net.serenitybdd.screenplay.rest.questions` | ### Shared across all styles | Class | Package | |---|---| | `@Managed`, `@Step`, `@Steps`, `@DefaultUrl`, `@WithTag` ... | `net.serenitybdd.annotations` (never `net.thucydides.core.annotations`) | | `SerenityJUnit5Extension` | `net.serenitybdd.junit5` | | `ScenarioSteps` (Page Objects) | `net.thucydides.core.steps` (**this one is correct** under `net.thucydides`) | ## 2. Common mistakes | Wrong | Right | |---|---| | `import net.serenitybdd.rest.abilities.*` or `net.serenitybdd.rest.questions.*` for Screenplay REST (those packages don't exist) | `net.serenitybdd.screenplay.rest.abilities.CallAnApi`, `...rest.questions.LastResponse` | | `Target.the("x").locatedBy(By.id("x"))` | `locatedBy("#x")`, a String; for a `By` use `located(By.id("x"))` | | `locatedBy("input.new-todo")` (tag-prefixed CSS; read as XPath, element not found at run time) | `located(By.cssSelector("input.new-todo"))` or `locatedBy(".new-todo")` | run-time failure, 5 of 66 unguided agentic runs | | Two `public` classes in one file, or a class name different from its file name | One public class per file, and the file name equals the class name | | `import net.serenitybdd.screenplay.webdriver.BrowseTheWeb` (package doesn't exist) | `net.serenitybdd.screenplay.abilities.BrowseTheWeb` | | `Question>` returned from `Text.ofEach(target)` (Selenium) | `Question>`. Playwright's `Text.ofEach` returns `List` | | `.viewedBy(actor)` on `Text`/`Value`/`Visibility`... | `actor.asksFor(Text.of(target))` or `Text.of(target).answeredBy(actor)`. `.viewedBy` is the old API | | `net.thucydides.core.annotations.DefaultUrl` (and `Step`, `Managed`) | `net.serenitybdd.annotations.*` | | JUnit 4 `@RunWith(SerenityRunner.class)` in a JUnit 5 project | `@ExtendWith(SerenityJUnit5Extension.class)` | | Moving `ScenarioSteps` to `net.serenitybdd...` | `net.thucydides.core.steps.ScenarioSteps` | | `OnlineCast` in the wrong package | `net.serenitybdd.screenplay.actors.OnlineCast` | | `@Page` annotation | Doesn't exist. In Page Object tests use `@Steps` for step libraries and extend `PageObject` for pages | | `Instrumented` as a class | `Tasks.instrumented(MyTask.class, args)` (static method), or prefer `Task.where(..)` | | `Ensure.thatTheCurrentUrl().contains(..)` or `Ensure.thatThePageTitle()` (Playwright) | Doesn't compile (package-private return types). Assert on `BrowseTheWebWithPlaywright.as(actor).getCurrentPage().url()` or on visible page content | 6 of the 8 guided Playwright compile failures inspected | | `WaitFor.selector("#books .book")` or `Ensure.that("#books .book").hasText(..)` where the selector matches several rows | `"#books .book >> nth=0"`, or `hasCount(n)`, or `Text.ofEach(..)` | strict mode violation at run time, 2 guided runs | | `Navigate` from a Selenium package | Selenium: `Open.url(..)`. `Navigate` exists only in Playwright | ## 3. Minimal working patterns The source files are under `` and are compile-checked. Names are the same across styles so you can compare them. | Style | Files | Key points | |---|---|---| | Screenplay + Selenium + JUnit 5 | `sp-selenium/todo/*` | `@ExtendWith(SerenityJUnit5Extension.class)`, `@Managed(driver="chrome") WebDriver`, `OnStage.setTheStage(new OnlineCast())`, `OnStage.theActorCalled("Toby")`, `Task.where(..)`, `Target.the(..).locatedBy(..)`, `seeThat(question, matcher)` or `Ensure.that(..)` | | Screenplay + Playwright + JUnit 5 | `sp-playwright/todo/*` (todo list, form with select and checkbox) | `new PlaywrightCast()`, string selectors, `Press.keys("Enter")`, `Ensure.that(selector).hasText(..)` | | Screenplay + REST + JUnit 5 | `sp-rest/api/*` | `Actor.named(..).whoCan(CallAnApi.at(baseUrl))`, `Get.resource(..)`, `Post.to(..).with(request -> ...)`, `LastResponse.received()` | | Cucumber + Screenplay | `cuke-sp/*` | `@Suite` + `@IncludeEngines("cucumber")` + `@SelectClasspathResource("features")`, plugin `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel`, a `{actor}` `@ParameterType`, `@Before` that sets the stage | ### Rules of thumb - Use `Task.where("{0} does something", step1, step2)` for tasks. Use a class with `performAs` only when the task needs real logic. - `actor.attemptsTo(..)` for actions. `actor.should(seeThat(..))` or `Ensure` for checks. - Keep locators in a `Target` holder class (Selenium) or constants (Playwright). - Build tasks and questions with a static factory (`AddATodo.called("Buy milk")`) so the test reads like a sentence. ## 4. Not yet verified - `Ensure` for web has several overloads. Only `that(Target).hasText(..)` is compile-tested. - Page Objects (Selenium and Playwright) are covered in `page-objects.md`, not here. - The examples were run once against the live sample sites with Serenity 5.4.1 (Playwright modules 5.3.11). Other sites and larger suites may behave differently. - `serenity-screenplay-playwright` is documented at 5.3.11, not 5.4.x. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `sp-selenium/todo/AddATodo.java` ```java package todo; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Enter; import org.openqa.selenium.Keys; public class AddATodo { public static Performable called(String item) { return Task.where("{0} adds a todo called " + item, Enter.theValue(item).into(TodoList.NEW_TODO).thenHit(Keys.ENTER)); } } ``` ### `sp-selenium/todo/AddTodoTest.java` ```java package todo; import net.serenitybdd.annotations.Managed; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.actions.Open; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.actors.OnlineCast; import net.serenitybdd.screenplay.ensure.Ensure; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.WebDriver; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.hasItem; @ExtendWith(SerenityJUnit5Extension.class) class AddTodoTest { @Managed(driver = "chrome") WebDriver browser; @BeforeEach void setTheStage() { OnStage.setTheStage(new OnlineCast()); } @Test void shouldShowTheNewTodo() { var toby = OnStage.theActorCalled("Toby"); toby.attemptsTo( Open.url("https://todo-app.serenity-js.org"), AddATodo.called("Buy milk")); toby.should(seeThat(TheTodos.displayed(), hasItem("Buy milk"))); } @Test void shouldShowTheNewTodoUsingEnsure() { var toby = OnStage.theActorCalled("Toby"); toby.attemptsTo( Open.url("https://todo-app.serenity-js.org"), AddATodo.called("Buy milk"), Ensure.that(TodoList.ITEMS).hasText("Buy milk")); } } ``` ### `sp-selenium/todo/TheTodos.java` ```java package todo; import net.serenitybdd.screenplay.Question; import net.serenitybdd.screenplay.questions.Text; import java.util.Collection; public class TheTodos { // Text.ofEach(..) is a Question> - not List public static Question> displayed() { return Text.ofEach(TodoList.ITEMS); } } ``` ### `sp-selenium/todo/TodoList.java` ```java package todo; import net.serenitybdd.screenplay.targets.Target; import org.openqa.selenium.By; public class TodoList { // locatedBy(String) treats a string as XPath whenever it compiles as one, so a CSS selector such as // "input.new-todo" (a valid XPath name) is NOT found. Use located(By.cssSelector(..)) for those. public static final Target NEW_TODO = Target.the("new todo field").located(By.cssSelector("input.new-todo")); public static final Target ITEMS = Target.the("todo items").locatedBy(".todo-list li label"); } ``` ### `sp-playwright/todo/AddTodoTest.java` ```java package todo; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.playwright.actors.PlaywrightCast; import net.serenitybdd.screenplay.playwright.assertions.Ensure; import net.serenitybdd.screenplay.playwright.interactions.Enter; import net.serenitybdd.screenplay.playwright.interactions.Open; import net.serenitybdd.screenplay.playwright.interactions.Press; import net.serenitybdd.screenplay.playwright.questions.Text; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.hasItem; @ExtendWith(SerenityJUnit5Extension.class) class AddTodoTest { static final String NEW_TODO = ".new-todo"; static final String ITEMS = ".todo-list li label"; static Performable addATodo(String item) { return Task.where("{0} adds a todo called " + item, Enter.theValue(item).into(NEW_TODO), Press.keys("Enter")); } @BeforeEach void setTheStage() { OnStage.setTheStage(new PlaywrightCast()); } @Test void shouldShowTheNewTodo() { var toby = OnStage.theActorCalled("Toby"); toby.attemptsTo( Open.url("https://todo-app.serenity-js.org"), addATodo("Buy milk"), Ensure.that(ITEMS).hasText("Buy milk")); // Playwright's Text.ofEach(..) is a Question> (Selenium's is Collection) toby.should(seeThat(Text.ofEach(ITEMS), hasItem("Buy milk"))); } } ``` ### `sp-playwright/todo/FormTest.java` ```java package todo; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.playwright.actors.PlaywrightCast; import net.serenitybdd.screenplay.playwright.assertions.Ensure; import net.serenitybdd.screenplay.playwright.interactions.Check; import net.serenitybdd.screenplay.playwright.interactions.Click; import net.serenitybdd.screenplay.playwright.interactions.Open; import net.serenitybdd.screenplay.playwright.interactions.SelectFromOptions; import net.serenitybdd.screenplay.playwright.interactions.WaitFor; import net.serenitybdd.screenplay.playwright.questions.Checked; import net.serenitybdd.screenplay.playwright.questions.SelectedOptionText; import net.serenitybdd.screenplay.playwright.questions.Value; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.equalTo; /** Playwright interactions and questions for forms. Selectors are Playwright selector strings. */ @ExtendWith(SerenityJUnit5Extension.class) class FormTest { // A self-contained page, so the example runs without a network or a site static final String FORM_PAGE = "data:text/html," + "" + ""; @BeforeEach void setTheStage() { OnStage.setTheStage(new PlaywrightCast()); } @Test void shouldFillInAForm() { var ana = OnStage.theActorCalled("Ana"); ana.attemptsTo( Open.url(FORM_PAGE), SelectFromOptions.byVisibleText("Canada").from("#country"), // also byValue(..), byIndex(..) Check.the("#terms"), // Uncheck.checkbox(..) to clear WaitFor.selector("#submit"), Click.on("#submit"), Ensure.that("#country").hasValue("ca"), Ensure.that("#terms").isChecked()); ana.should( seeThat(SelectedOptionText.of("#country"), equalTo("Canada")), seeThat(Value.of("#country"), equalTo("ca")), seeThat(Checked.of("#terms"))); } } ``` ### `sp-playwright/todo/ListTest.java` ```java package todo; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.playwright.actors.PlaywrightCast; import net.serenitybdd.screenplay.playwright.assertions.Ensure; import net.serenitybdd.screenplay.playwright.interactions.Open; import net.serenitybdd.screenplay.playwright.interactions.WaitFor; import net.serenitybdd.screenplay.playwright.questions.Text; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import java.util.List; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.contains; /** * A selector used by WaitFor, Click, Ensure.hasText and Text.of must match exactly ONE element (Playwright's strict mode). * For a list, wait on the first row, count with hasCount, read all texts with Text.ofEach, or pick one with ">> nth=N". */ @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Reading a list on a page") class ListTest { static final String LIST_PAGE = "data:text/html," + "" + "" + "
Fluent Python
Clean Code
Refactoring
"; @BeforeEach void setTheStage() { OnStage.setTheStage(new PlaywrightCast()); } @Test @DisplayName("The rows of a list can be counted, read and picked by position") void rowsCanBeCountedReadAndPickedByPosition() { var ana = OnStage.theActorCalled("Ana"); ana.attemptsTo( Open.url(LIST_PAGE), WaitFor.selector("#rows .row >> nth=0"), // not "#rows .row": that matches 3 elements and fails Ensure.that("#rows .row").hasCount(3), // hasCount is the check for "how many" Ensure.that("#rows .row >> nth=0 >> .title").hasText("Fluent Python"), Ensure.that("#rows .row >> nth=-1 >> .title").hasText("Refactoring")); ana.should(seeThat(Text.ofEach("#rows .title"), contains("Fluent Python", "Clean Code", "Refactoring"))); } } ``` ### `sp-rest/api/UsersApi.java` ```java package api; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.rest.interactions.Get; import net.serenitybdd.screenplay.rest.interactions.Post; public class UsersApi { public static Performable fetchUser(int id) { return Task.where("{0} fetches user " + id, Get.resource("/users/" + id)); } public static Performable createPost(String title) { return Task.where("{0} creates a post titled " + title, Post.to("/posts").with(request -> request .header("Content-Type", "application/json") .body("{\"title\":\"" + title + "\",\"body\":\"text\",\"userId\":1}"))); } } ``` ### `sp-rest/api/UsersApiTest.java` ```java package api; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.rest.abilities.CallAnApi; import net.serenitybdd.screenplay.rest.questions.LastResponse; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.equalTo; @ExtendWith(SerenityJUnit5Extension.class) class UsersApiTest { Actor alice; @BeforeEach void setUp() { alice = Actor.named("Alice").whoCan(CallAnApi.at("https://jsonplaceholder.typicode.com")); } @Test void shouldFindUserOne() { alice.attemptsTo(UsersApi.fetchUser(1)); alice.should( seeThat("the status code", actor -> LastResponse.received().answeredBy(actor).statusCode(), equalTo(200)), seeThat("the user name", actor -> LastResponse.received().answeredBy(actor).path("name"), equalTo("Leanne Graham"))); } @Test void shouldCreateAPost() { alice.attemptsTo(UsersApi.createPost("Hello")); alice.should(seeThat("the status code", actor -> LastResponse.received().answeredBy(actor).statusCode(), equalTo(201))); } } ``` ### `cuke-sp/features/add_todo.feature` ```gherkin Feature: Todo list Scenario: Adding a todo Given Toby has opened the todo app When Toby adds "Buy milk" to the list Then Toby should see "Buy milk" in the list ``` ### `cuke-sp/todo/AddATodo.java` ```java package todo; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Enter; import org.openqa.selenium.Keys; public class AddATodo { public static Performable called(String item) { return Task.where("{0} adds a todo called " + item, Enter.theValue(item).into(TodoList.NEW_TODO).thenHit(Keys.ENTER)); } } ``` ### `cuke-sp/todo/RunCucumberTests.java` ```java package todo; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "todo") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty") public class RunCucumberTests { } ``` ### `cuke-sp/todo/TheTodos.java` ```java package todo; import net.serenitybdd.screenplay.Question; import net.serenitybdd.screenplay.questions.Text; import java.util.Collection; public class TheTodos { // Text.ofEach(..) is a Question> - not List public static Question> displayed() { return Text.ofEach(TodoList.ITEMS); } } ``` ### `cuke-sp/todo/TodoList.java` ```java package todo; import net.serenitybdd.screenplay.targets.Target; public class TodoList { public static final Target NEW_TODO = Target.the("new todo field").locatedBy(".new-todo"); public static final Target ITEMS = Target.the("todo items").locatedBy(".todo-list li label"); } ``` ### `cuke-sp/todo/TodoStepDefinitions.java` ```java package todo; import io.cucumber.java.Before; import io.cucumber.java.ParameterType; 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.actions.Open; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.actors.OnlineCast; import static net.serenitybdd.screenplay.GivenWhenThen.seeThat; import static org.hamcrest.Matchers.hasItem; public class TodoStepDefinitions { @Before public void setTheStage() { OnStage.setTheStage(new OnlineCast()); } @ParameterType(".*") public Actor actor(String name) { return OnStage.theActorCalled(name); } @Given("{actor} has opened the todo app") public void opensTheApp(Actor actor) { actor.attemptsTo(Open.url("https://todo-app.serenity-js.org")); } @When("{actor} adds {string} to the list") public void addsATodo(Actor actor, String item) { actor.attemptsTo(AddATodo.called(item)); } @Then("{actor} should see {string} in the list") public void seesTheTodo(Actor actor, String item) { actor.should(seeThat(TheTodos.displayed(), hasItem(item))); } } ``` --- # Cucumber: Runner, Tags, Hooks, Gradle and Parallel Verified against Serenity **5.4.1** (Cucumber 8.0.4, JUnit 6.1.3). Every command on this page was run against the example at the end of the page. ## 1. Setup - **Maven:** the Cucumber row of the dependency table on the project-setup page: `serenity-core`, `serenity-junit5`, `serenity-cucumber`, `io.cucumber:cucumber-java`, `io.cucumber:cucumber-junit-platform-engine`, `junit-platform-suite`. Use the JUnit BOM, and use the Cucumber version Serenity brings. - **Gradle:** the `build.gradle` at the end of this page is a working build (below). - Feature files go in `src/test/resources/features`, step definitions and hooks in a package under `src/test/java`. ## 2. The runner A JUnit Platform suite. No `@RunWith`: ```java @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "demo") // the package(s) with steps and hooks @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty") public class RunCucumberTests { } ``` - The plugin class is `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel`. It connects Cucumber to Serenity: without it, Serenity APIs used in steps fail with "No BaseStepListener has been registered". - Name the class so that Maven's test plugin runs it: ending in `Test` or `Tests` (`RunCucumberTests`). A name such as `CucumberSuite` is skipped silently (see the troubleshooting page, "Results: 0 tests"). - `@RunWith(CucumberWithSerenity.class)` (from `net.serenitybdd.cucumber`) is the JUnit 4 style runner. In a JUnit 5 project use the suite above. ## 3. Running part of the suite All of these were run against the example. Add them to the Maven command line: | Goal | Option | Scenarios run (of 5) | |---|---|---| | Only `@smoke` | `-Dcucumber.filter.tags=@smoke` | | Everything except `@wip` | `-Dcucumber.filter.tags="not @wip"` | | Tag expression | `-Dcucumber.filter.tags="@basket and not @wip"` | | By scenario name (regular expression) | `-Dcucumber.filter.name="Adding passes"` | | One feature file | `-Dcucumber.features=src/test/resources/features/basket.feature` | The same filters can be fixed in the project instead of the command line: - in the runner: `@ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "not @wip")` (`Constants.FILTER_TAGS_PROPERTY_NAME`, the same class as `GLUE_PROPERTY_NAME`): 4 scenarios, or - in `src/test/resources/junit-platform.properties`: `cucumber.filter.tags=not @wip`: 4 scenarios. When `cucumber.features` is given, Cucumber ignores the runner's `@SelectClasspathResource` and prints a warning saying so; that is expected. **Re-running failed scenarios.** Add the Cucumber `rerun` plugin to the runner (`...SerenityReporterParallel,pretty,rerun:target/rerun.txt`). After a run that has failures, `target/rerun.txt` lists them (`classpath:features/failing.feature:3`). Run only those with `-Dcucumber.features=@target/rerun.txt` (verified: 6 scenarios, then 1). ## 4. Gradle the `build.gradle` at the end of this page (Serenity Gradle plugin `net.serenity-bdd.serenity-gradle-plugin` 5.4.1, Gradle 9.8.1 tested, tasks `test` and `aggregate`) was run: `gradle clean test` ran the 5 scenarios and `aggregate` produced `target/site/serenity/index.html`. **Gradle does not pass `-D` options to the test JVM.** `gradle test -Dcucumber.filter.tags=@smoke` runs every scenario unless the build forwards the option (verified: 4 outcomes without it, 1 with it): ```groovy test { useJUnitPlatform() systemProperties System.properties.findAll { k, v -> k.toString().startsWith('cucumber.') } finalizedBy 'aggregate' } ``` The same applies to any other `-D` option your tests read. `ignoreFailures = true` in that file lets the report task run when a scenario fails (the same idea as `-Dmaven.test.failure.ignore=true` for Maven); leave it out if the build must fail. ## 5. Scenario outlines, data tables and parameter types `cuke-plain/features/maths.feature` has a `Scenario Outline` with an `Examples` table (2 rows) and `basket.feature` has a `Background` and a data table. In the steps (`BasketSteps.java`): - a data table argument is `io.cucumber.datatable.DataTable` (`items.asMaps()` gives a list of rows); - a custom parameter type is a method annotated `@ParameterType("\\d+\\.\\d+")` that returns the type (`{money}` in the step text gives a `BigDecimal`); - Maven counts each example row as a scenario (the example has 5: 1 + 2 + 2); the Serenity outcome files (`target/site/serenity/*.json`) count the outline once (4 files for these 5 scenarios), so two counts of the same run can differ. ## 6. Hooks `cuke-plain/demo/Hooks.java`: - `@Before` and `@After` from `io.cucumber.java`; they take an optional `io.cucumber.java.Scenario` argument (`getName()`, `getSourceTagNames()`, `isFailed()`, `getStatus()`). - `@Before("@basket")` runs only for scenarios with that tag; `order` sorts hooks: `@Before` hooks run from the lowest order to the highest, `@After` hooks from the highest to the lowest (verified: before-1, before-5, after-5, after-1). - `Serenity.recordReportData().withTitle("..").andContents("..")` adds data to the scenario in the Serenity report (checked: it appears in the outcome files). ## 7. Sharing state between steps Cucumber creates new step-definition objects for every scenario. Three ways to share state inside one scenario: 1. **Fields of one step class.** Fine when all the steps are in that class. 2. **Serenity session variables** (used in the example): `Serenity.setSessionVariable("basketTotal").to(value)` and `Serenity.sessionVariableCalled("basketTotal")`. They are per scenario (and per thread) and work from any class. 3. **Dependency injection with PicoContainer:** add `io.cucumber:cucumber-picocontainer` (same version as Cucumber), give each step class a constructor that takes the shared object, and Cucumber creates one instance per scenario (verified with two step classes sharing a `Shared` object). Do not use static fields to share scenario state: with parallel execution scenarios overwrite each other. ## 8. Parallel execution `src/test/resources/junit-platform.properties`: ``` cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 ``` Verified: the 5 scenarios ran and the report was produced. Keep in mind that each scenario needs its own state (section 7) and, in a web test, its own browser. Reports of parallel runs are a common source of problems; the troubleshooting page says what has and has not been reproduced. ## 9. Cucumber with Screenplay Put `OnStage.setTheStage(new OnlineCast())` in a `@Before` hook and a `@ParameterType` named `actor` that returns `OnStage.theActorCalled(name)`, as in the Screenplay page's Cucumber example. ## 10. Cucumber with page objects and a step library (no Screenplay) The glue code is thin: each step definition calls one `@Step` method of a step library, and the step library uses a page object. The example `cuke-pages` has the whole chain (`SignInStepDefinitions` -> `ReaderSteps` -> `SignInPage`) and its 3 scenarios pass in headless Chrome; the verification script in the serenity-core repository (`docs/ai/skill`) runs the claims below. - Declare the library in the step-definition class with **`@Steps ReaderSteps reader;`** (`net.serenitybdd.annotations.Steps`). Declare the page object as a plain field of the step library (`SignInPage signInPage;`); Serenity creates it and gives it the driver. With `ReaderSteps reader = new ReaderSteps();` the page field stays null and the scenario errors with `NullPointerException: ... "this.signInPage" is null`. - Page objects extend `net.serenitybdd.core.pages.PageObject` and use `@FindBy` with `WebElementFacade`; no `@Managed` is needed in Cucumber. See `page-objects.md` for the idioms. - The report shows both layers: the Gherkin line ("When Reader signs in as ...") with the `@Step` text nested under it ("Sign in as reader@example.com with the password wrong"). Write the `@Step` text in business language too. - The runner and plugin are the same as in section 2: the plugin class is `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel`. ### Optional text `book(s)` in a Cucumber expression In a step written `@Then("the catalogue lists {int} book(s)")`, `(s)` is **optional text**: the step matches "5 book" and "5 books". It does **not** match a feature line that contains the characters `book(s)`. Reproduced: a feature saying `the catalogue lists 5 book(s)` gave `The step 'the catalogue lists 5 book(s)' is undefined.` and Maven failed the build, while the Serenity report shows the scenario as **pending** (not failed), so look at the console output as well as the report. Write the feature in plain words ("1 book", "5 books"); to match a literal `(s)`, escape it in the step as `book\\(s)`. ## 11. Not covered here Gherkin in other languages, `@Before` for browser start-up in cloud grids, the Cucumber `Transpose`/`DocString` types, Spring or Guice object factories, and Cucumber-JVM versions other than the one Serenity brings. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `build.gradle` ```groovy plugins { id 'java' id 'net.serenity-bdd.serenity-gradle-plugin' version '5.4.1' } repositories { mavenCentral() } java { sourceCompatibility = JavaVersion.VERSION_17 targetCompatibility = JavaVersion.VERSION_17 } dependencies { // One JUnit version for every JUnit artifact (Serenity 5.4.1 brings JUnit 6.1.3) testImplementation platform('org.junit:junit-bom:6.1.3') testImplementation 'org.junit.jupiter:junit-jupiter' testImplementation 'org.junit.platform:junit-platform-suite' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' testImplementation 'net.serenity-bdd:serenity-core:5.4.1' testImplementation 'net.serenity-bdd:serenity-junit5:5.4.1' testImplementation 'net.serenity-bdd:serenity-cucumber:5.4.1' testImplementation 'io.cucumber:cucumber-java:8.0.4' testImplementation 'io.cucumber:cucumber-junit-platform-engine:8.0.4' testImplementation 'org.assertj:assertj-core:3.27.7' testRuntimeOnly 'org.slf4j:slf4j-simple:2.0.17' } test { useJUnitPlatform() ignoreFailures = true // so that the report task still runs when a scenario fails // Gradle does not pass -D options on to the test JVM: forward the Cucumber ones systemProperties System.properties.findAll { k, v -> k.toString().startsWith('cucumber.') } finalizedBy 'aggregate' // build the Serenity report after the tests } ``` ### `cuke-plain/demo/BasketSteps.java` ```java package demo; import io.cucumber.datatable.DataTable; import io.cucumber.java.ParameterType; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import net.serenitybdd.core.Serenity; import java.math.BigDecimal; import java.util.List; import java.util.Map; import static org.assertj.core.api.Assertions.assertThat; public class BasketSteps { // A custom parameter type: "3.70 dollars" -> BigDecimal @ParameterType("\\d+\\.\\d+") public BigDecimal money(String amount) { return new BigDecimal(amount); } @Given("an empty basket") public void anEmptyBasket() { // Serenity session variables live for one scenario and can be read from any step class Serenity.setSessionVariable("basketTotal").to(BigDecimal.ZERO); } @When("I add these items:") public void iAddTheseItems(DataTable items) { List> rows = items.asMaps(); for (Map row : rows) { addToTotal(new BigDecimal(row.get("price"))); } } @When("I add a {string} costing {money} dollars") public void iAddACosting(String name, BigDecimal price) { addToTotal(price); } @Then("the basket total should be {money} dollars") public void theTotalShouldBe(BigDecimal expected) { BigDecimal total = Serenity.sessionVariableCalled("basketTotal"); assertThat(total).isEqualByComparingTo(expected); } private void addToTotal(BigDecimal price) { BigDecimal total = Serenity.sessionVariableCalled("basketTotal"); Serenity.setSessionVariable("basketTotal").to(total.add(price)); } } ``` ### `cuke-plain/demo/Hooks.java` ```java package demo; import io.cucumber.java.After; import io.cucumber.java.Before; import io.cucumber.java.Scenario; import net.serenitybdd.core.Serenity; public class Hooks { // Runs before every scenario. The Scenario parameter is optional. @Before(order = 0) public void beforeEachScenario(Scenario scenario) { Serenity.recordReportData().withTitle("Scenario tags").andContents(String.join(", ", scenario.getSourceTagNames())); } // Only for scenarios tagged @basket @Before(value = "@basket", order = 1) public void beforeBasketScenarios() { Serenity.recordReportData().withTitle("Setup").andContents("basket scenario"); } @After public void afterEachScenario(Scenario scenario) { if (scenario.isFailed()) { Serenity.recordReportData().withTitle("Failed scenario").andContents(scenario.getName()); } } } ``` ### `cuke-plain/demo/MathSteps.java` ```java package demo; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import static org.assertj.core.api.Assertions.assertThat; public class MathSteps { int sum; @Given("I have {int} and {int}") public void have(int a, int b) { sum = a + b; } @Then("the sum should be {int}") public void sumIs(int expected) { assertThat(sum).isEqualTo(expected); } } ``` ### `cuke-plain/demo/RunCucumberTests.java` ```java package demo; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "demo") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty") public class RunCucumberTests { } ``` ### `cuke-plain/features/basket.feature` ```gherkin @basket Feature: Shopping basket Background: Given an empty basket Scenario: Adding items When I add these items: | name | price | | Tea | 2.50 | | Bread | 1.20 | Then the basket total should be 3.70 dollars @wip Scenario: Work in progress When I add a "Cake" costing 4.00 dollars Then the basket total should be 4.00 dollars ``` ### `cuke-plain/features/maths.feature` ```gherkin Feature: Arithmetic @smoke Scenario: Adding passes Given I have 2 and 2 Then the sum should be 4 Scenario Outline: Adding several pairs Given I have and Then the sum should be Examples: | a | b | sum | | 1 | 1 | 2 | | 2 | 3 | 5 | ``` ### `cuke-pages/features/sign_in.feature` ```gherkin Feature: Signing in to the bookshop Scenario: A reader with the right password reaches the catalogue Given Reader is on the sign-in page When Reader signs in as "reader@example.com" with the password "correct-horse" Then the Catalogue page is shown Scenario: A reader with the wrong password is refused Given Reader is on the sign-in page When Reader signs in as "reader@example.com" with the password "wrong" Then Reader is told "Invalid email or password" Scenario Outline: The catalogue says how many books it holds Given Reader is on the sign-in page When Reader signs in as "reader@example.com" with the password "correct-horse" Then the catalogue lists books Examples: | count | | 5 | ``` ### `cuke-pages/shop/ReaderSteps.java` ```java package shop; import net.serenitybdd.annotations.Step; import static org.assertj.core.api.Assertions.assertThat; /** The step library: business-level steps that use the page object. Cucumber's glue code calls these. */ public class ReaderSteps { SignInPage signInPage; // page objects are created for you when declared as fields @Step("Open the sign-in page") public void opensTheSignInPage() { signInPage.show(); } @Step("Sign in as {0} with the password {1}") public void signsInWith(String email, String password) { signInPage.signInWith(email, password); } @Step("The {0} page should be shown") public void shouldSeeThePage(String name) { assertThat(signInPage.headingText()).isEqualTo(name); } @Step("The message {0} should be shown") public void shouldBeToldThat(String message) { assertThat(signInPage.messageText()).isEqualTo(message); } } ``` ### `cuke-pages/shop/RunCucumberTests.java` ```java package shop; import org.junit.platform.suite.api.ConfigurationParameter; import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = GLUE_PROPERTY_NAME, value = "shop") @ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty") public class RunCucumberTests { } ``` ### `cuke-pages/shop/SignInPage.java` ```java package shop; import net.serenitybdd.core.pages.PageObject; import net.serenitybdd.core.pages.WebElementFacade; import org.openqa.selenium.support.FindBy; // A small page served from a data: URL, so the example needs no server or network. public class SignInPage extends PageObject { static final String HTML = "Bookshop" + "

Sign in

" + "" + "" + "

"; @FindBy(id = "email") WebElementFacade email; @FindBy(id = "password") WebElementFacade password; @FindBy(id = "go") WebElementFacade signInButton; @FindBy(id = "heading") WebElementFacade heading; @FindBy(id = "error") WebElementFacade message; public void show() { getDriver().get("data:text/html;charset=utf-8," + HTML.replace("#", "%23")); } public void signInWith(String emailAddress, String secret) { email.type(emailAddress); password.type(secret); signInButton.click(); } public String headingText() { return heading.getText(); } public String messageText() { return message.getText(); } } ``` ### `cuke-pages/shop/SignInStepDefinitions.java` ```java package shop; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import net.serenitybdd.annotations.Steps; public class SignInStepDefinitions { @Steps ReaderSteps reader; @Given("Reader is on the sign-in page") public void readerIsOnTheSignInPage() { reader.opensTheSignInPage(); } @When("Reader signs in as {string} with the password {string}") public void readerSignsIn(String email, String password) { reader.signsInWith(email, password); } @Then("the {word} page is shown") public void thePageIsShown(String name) { reader.shouldSeeThePage(name); } @Then("Reader is told {string}") public void readerIsTold(String message) { reader.shouldBeToldThat(message); } // "book(s)" is optional text in a Cucumber expression: this step matches "5 book" and "5 books", // but NOT the literal text "5 book(s)" if a feature file contains it. @Then("the catalogue lists {int} book(s)") public void theCatalogueLists(int count) { reader.shouldBeToldThat(count + " books"); } } ``` --- # Page Objects: Imports and Idioms Verified against Serenity **5.4.1** (Playwright module: **5.3.11**, the latest published). The examples at the end of this page compile and their tests pass. Page Objects with step libraries work with Selenium and with Playwright, with JUnit 5. This is as valid as Screenplay. Use the style the user asked for or the project already uses, and don't mix Page Objects and Screenplay in one test. ## 1. Structure ``` Test (@Steps field) -> Step library (@Step methods) -> Page Object (locators and page actions) ``` Tests call step methods that read like business actions. Page Objects hide locators. ## 2. Where things live | Class | Package | |---|---| | `@Step`, `@Steps`, `@Managed`, `@DefaultUrl`, `@WhenPageOpens`, `@WithTag`, `@Title`, `@Narrative`, `@Pending` | `net.serenitybdd.annotations` (**never** `net.thucydides.core.annotations`) | | `SerenityJUnit5Extension` | `net.serenitybdd.junit5` | | `PageObject`, `WebElementFacade` | `net.serenitybdd.core.pages` | | `UIInteractionSteps` (base class for Selenium step libraries) | `net.serenitybdd.core.steps` | | `Serenity` (static facade) | `net.serenitybdd.core.Serenity` | | `ScenarioSteps` (older step library base class, still valid) | `net.thucydides.core.steps` | | `Pages` | `net.thucydides.core.pages` | | `@FindBy` | `org.openqa.selenium.support.FindBy` (Serenity also has `net.serenitybdd.core.annotations.findby.FindBy`) | `net.thucydides.core.pages.WebElementFacade` and `net.thucydides.core.annotations.findby.FindBy` are deprecated aliases. Use the `net.serenitybdd` ones. Maven: Selenium needs `serenity-core` and `serenity-junit5`. Playwright adds `serenity-playwright` and `com.microsoft.playwright:playwright`. ## 3. Selenium Page Objects Files: `po-selenium/todo/{TodoPage,TodoSteps,AddTodoTest}.java` - Page: `@DefaultUrl("https://...")`, `extends PageObject`, `@FindBy(css = "...") WebElementFacade field;`, or `$("css")`, `$$("css")` for lookups. **A String that starts with a tag name is not read as CSS.** `$("button")`, `$("th[data-sort='title']")` and `thenFind("button")` compile as XPath, so they look for the wrong thing and fail with `ElementShouldBeEnabledException` or `NoSuchElementException ... xpath`. Start the selector with `#` or `.` (`$("#go")`, `$(".sort")`), or use `$(By.cssSelector("button"))` (for a child element `row.findBy(By.cssSelector("button"))`; `thenFind` has no `By` version), or write a real XPath. The same rule applies to Screenplay `Target.the("name").locatedBy(..)`. - **Don't write your own `open()`.** `PageObject.open()` already exists and opens the `@DefaultUrl`; it is `final`, so redeclaring it fails to compile. For another URL use `openAt("https://...")`, or a differently named method. - `@FindBy(css = ...)` takes a String. `$(By.cssSelector(..))` takes a `By`. Don't mix them up. - Step library: `extends UIInteractionSteps`, a field for each page object (injected automatically), and `@Step("Add the todo '{0}'")` on public methods. `{0}` is the first argument. - Test: `@ExtendWith(SerenityJUnit5Extension.class)`, `@Managed(driver = "chrome") WebDriver driver;`, `@Steps TodoSteps todo;`. Assertions in the test with AssertJ or Hamcrest, or with `assertThat` inside a `@Step` method. - Useful `WebElementFacade` methods: `type(..)`, `typeAndEnter(..)`, `click()`, `getText()`, `isVisible()`, `waitUntilVisible()`, `selectByVisibleText(..)`, `getValue()`. ## 4. Playwright Page Objects Files: `po-playwright/todo/{TodoPage,TodoSteps,AddTodoTest}.java` - The Page Object is a plain class that holds a `com.microsoft.playwright.Page`. It does **not** extend `PageObject`, and doesn't use `@FindBy`, `WebElementFacade` or `@DefaultUrl`. Locators are Playwright `page.locator("css")`, `getByRole(..)`. - A locator used with `click()`, `fill()`, `textContent()` or `waitFor()` must match exactly one element (strict mode); for lists use `.first()`, `.nth(i)`, `.count()` or `allTextContents()`. **Don't build a Playwright text pattern with `Pattern.quote(..)`.** Playwright sends the pattern to the browser's JavaScript engine, which doesn't understand Java's `\Q..\E`, so `setHasText(Pattern.compile("^\\s*" + Pattern.quote(title) + "\\s*$"))` matches nothing and the step times out after 30 seconds (two Opus runs). Use the plain string (`setHasText(title)` matches by substring, ignoring case; `text-is('..')` / `has-text('..')` in a selector string match exactly or by substring), or write the pattern by hand (`Pattern.compile("^\\s*Clean Code\\s*$")`). Run in the verification script in the serenity-core repository (`docs/ai/skill`). - Playwright Java API details models get wrong (run in the verification script in the serenity-core repository (`docs/ai/skill`)): `page.getByRole(role, new Page.GetByRoleOptions()...)` but `locator.getByRole(role, new Locator.GetByRoleOptions()...)`: passing the `Page` options to a locator is a compile error; and as a selector string a role needs the prefix, `"role=heading[name=\"Sign in\"]"`: `"heading[name=...]"` throws `InvalidSelectorError`. - The test owns the browser: start `Playwright` and the `Browser` in `@BeforeAll`, and in `@BeforeEach` create a page and call `PlaywrightSerenity.registerPage(page)`. In `@AfterEach` call `PlaywrightSerenity.unregisterPage(page)` and close the page. - Step library: a plain class with `@Step` methods that get the page from `PlaywrightSerenity.getCurrentPage()` (or take a `Page` parameter) and wrap it in the Page Object. No `UIInteractionSteps`. - `PlaywrightSerenity` is in `net.serenitybdd.playwright`. A `@SerenityPlaywright` meta-annotation (`net.serenitybdd.playwright.junit5`) combines the extensions. See `serenity-playwright-demo` for it. ## 5. Common mistakes | Wrong | Right | |---|---| | `import net.thucydides.core.annotations.{Step,Steps,Managed,DefaultUrl}` (most Page Object failures) | `net.serenitybdd.annotations.*` | | Declaring `public void open()` in a page object | Delete it. Use the inherited `open()` with `@DefaultUrl`, or `openAt(url)` | | `@FindBy("...")` or passing a String where a `By` is needed (and the reverse) | `@FindBy(css = "...")`; `$("css")` for Strings; `$(By.id("x"))` for a `By` | | `$("button")`, `$("th[data-sort='title']")`, `row.thenFind("button")` (read as XPath) | `$("#go")`, `$(By.cssSelector("button"))`, `row.findBy(By.cssSelector("button"))` | | JUnit 4 `@RunWith(SerenityRunner.class)` | `@ExtendWith(SerenityJUnit5Extension.class)` | | `@Page` annotation | Doesn't exist. Use a page object field in the step library, or `@Steps` for step libraries in the test | | `ScenarioSteps` from `net.serenitybdd...` | `net.thucydides.core.steps.ScenarioSteps` (this one *is* under `net.thucydides`) | | Extending `PageObject` for a Playwright page, or `Page` from Selenium | Playwright pages are plain classes using `com.microsoft.playwright.Page` | ## 6. Not yet verified - Runtime behaviour beyond the examples: they ran once against the live todo app. - `@WhenPageOpens`, `waitFor`/`waitUntil*` helpers and `Pages`-based injection are not covered by examples. - The Playwright section is based on 5.3.11 and the `serenity-playwright-demo` module. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `po-selenium/todo/AddTodoTest.java` ```java package todo; import net.serenitybdd.annotations.Managed; import net.serenitybdd.annotations.Steps; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.WebDriver; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class AddTodoTest { @Managed(driver = "chrome") WebDriver driver; @Steps TodoSteps todo; @Test void shouldShowTheNewTodo() { todo.openTheApplication(); todo.addTodo("Buy milk"); assertThat(todo.todoItems()).contains("Buy milk"); } } ``` ### `po-selenium/todo/TodoPage.java` ```java package todo; import net.serenitybdd.annotations.DefaultUrl; import net.serenitybdd.core.pages.PageObject; import net.serenitybdd.core.pages.WebElementFacade; import org.openqa.selenium.support.FindBy; import java.util.List; // Do not declare your own open(): PageObject already has open() and it uses @DefaultUrl. @DefaultUrl("https://todo-app.serenity-js.org") public class TodoPage extends PageObject { @FindBy(css = ".new-todo") WebElementFacade newTodoField; public void addTodo(String item) { newTodoField.typeAndEnter(item); } public List todoItems() { return $$(".todo-list li label").stream().map(WebElementFacade::getText).toList(); } } ``` ### `po-selenium/todo/TodoSteps.java` ```java package todo; import net.serenitybdd.annotations.Step; import net.serenitybdd.core.steps.UIInteractionSteps; import java.util.List; public class TodoSteps extends UIInteractionSteps { TodoPage todoPage; // page objects are injected into step libraries @Step("Open the todo application") public void openTheApplication() { todoPage.open(); } @Step("Add the todo '{0}'") public void addTodo(String item) { todoPage.addTodo(item); } public List todoItems() { return todoPage.todoItems(); } } ``` ### `po-playwright/todo/AddTodoTest.java` ```java package todo; import com.microsoft.playwright.Browser; import com.microsoft.playwright.BrowserType; import com.microsoft.playwright.Page; import com.microsoft.playwright.Playwright; import net.serenitybdd.annotations.Steps; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.playwright.PlaywrightSerenity; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class AddTodoTest { @Steps TodoSteps todo; static Playwright playwright; static Browser browser; Page page; @BeforeAll static void startBrowser() { playwright = Playwright.create(); browser = playwright.chromium().launch(new BrowserType.LaunchOptions().setHeadless(true)); } @AfterAll static void stopBrowser() { browser.close(); playwright.close(); } @BeforeEach void openPage() { page = browser.newPage(); PlaywrightSerenity.registerPage(page); // lets Serenity report screenshots and steps } @AfterEach void closePage() { PlaywrightSerenity.unregisterPage(page); page.close(); } @Test void shouldShowTheNewTodo() { todo.openTheApplication(); todo.addTodo("Buy milk"); assertThat(todo.todoItems()).contains("Buy milk"); } } ``` ### `po-playwright/todo/TodoPage.java` ```java package todo; import com.microsoft.playwright.Page; import java.util.List; /** A plain class that wraps a Playwright Page. It does not extend Serenity's PageObject. */ public class TodoPage { private final Page page; public TodoPage(Page page) { this.page = page; } public void open() { page.navigate("https://todo-app.serenity-js.org"); } public void addTodo(String item) { page.locator(".new-todo").fill(item); page.locator(".new-todo").press("Enter"); } public List todoItems() { return page.locator(".todo-list li label").allInnerTexts(); } } ``` ### `po-playwright/todo/TodoSteps.java` ```java package todo; import com.microsoft.playwright.Page; import net.serenitybdd.annotations.Step; import net.serenitybdd.playwright.PlaywrightSerenity; import java.util.List; public class TodoSteps { private TodoPage todoPage() { Page page = PlaywrightSerenity.getCurrentPage(); // the page registered by the test return new TodoPage(page); } @Step("Open the todo application") public void openTheApplication() { todoPage().open(); } @Step("Add the todo '{0}'") public void addTodo(String item) { todoPage().addTodo(item); } public List todoItems() { return todoPage().todoItems(); } } ``` --- # REST with SerenityRest and a Step Library Verified against Serenity **5.4.1**. The example at the end of this page compiles and its tests pass; each claim was run. This is one of several valid ways to test a REST API with Serenity. The other is Screenplay with `CallAnApi` (see `screenplay.md` and `sp-rest/`). Use the one the user asked for or the project already uses; don't mix them. ## What you need Artifact `serenity-rest-assured` (it brings REST-assured and Hamcrest), plus `serenity-core` and `serenity-junit5` as in `project-setup.md`. Do not pin REST-assured or Groovy versions (see `troubleshooting.md`). ## Where things live | Class | Package | |---|---| | `SerenityRest` (`given()`, `then()`, `lastResponse()`, `reset()`) | `net.serenitybdd.rest` | | `@Step` | `net.serenitybdd.annotations` | | `@Steps` | `net.serenitybdd.annotations` | | `SerenityJUnit5Extension` | `net.serenitybdd.junit5` | | `ScenarioSteps` (optional base class, not needed) | `net.thucydides.core.steps` | ## The pattern 1. **A step library**: a plain class whose methods are annotated `@Step("business wording")`. The REST calls go inside the methods, using `SerenityRest.given()...when().get(..)`. Use `SerenityRest`, not `io.restassured.RestAssured`: it is the same API, but Serenity records every request and response in the report. 2. **The test** declares `@Steps BookshopApi bookshop;` (Serenity creates the library) and reads like a story: `bookshop.listTheBooks(); bookshop.responseShouldHaveStatus(200);`. 3. **Checks go in `@Step` methods too**, using `SerenityRest.then().statusCode(..)` and `.body("path", matcher)`, so the report shows "The response should have status 200". `SerenityRest.then()` checks the last response of the current thread. The base URI is not remembered between calls unless you keep it in the step library (as the example does) or set `SerenityRest`/`RestAssured.baseURI`. ## Mistakes models make | Wrong | Right | |---|---| | `import net.thucydides.core.annotations.Step` or `...annotations.Steps` (compile error: cannot find symbol) | `net.serenitybdd.annotations.Step`, `net.serenitybdd.annotations.Steps` | | `import io.restassured.RestAssured.given` for calls that should appear in the report | `net.serenitybdd.rest.SerenityRest.given` | | Creating the library with `new BookshopApi()` in the test | `@Steps BookshopApi bookshop;`. With `new`, the business steps vanish from the report and only raw `GET http://...` lines remain. | | `@Step` missing on a method | The method still runs but the report shows only the raw `GET http://...` line instead of your wording. | | `body("total", equalTo(90.0))` or `closeTo(90.0, 0.001)` | JSON numbers with a decimal point come back as `Float`, so a `Double` matcher fails (`Expected: <90.0>`, `Expected: a numeric value within <0.001> of <90.0>`). Use `equalTo(90.0f)`, or read the value as a double: `SerenityRest.lastResponse().jsonPath().getDouble("total")` and assert on that (AssertJ). | | Extending `ScenarioSteps` because old guides did | Not needed. A plain class works. If you do extend it, the package is `net.thucydides.core.steps.ScenarioSteps`. | ## Wording for the living documentation Name steps for what a person does or sees, not for the HTTP call: "Look up book number 2", "The response should show the title Clean Code", not "GET /api/books/2" or "Check JSON path title". Give each `@Test` a `@DisplayName` that states the business outcome, and the class a `@DisplayName` naming the feature (see `living-documentation.md`). ## Examples | Style | Files | |---|---| | REST + step library + JUnit 5 | `rest-steps/api/*` | ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `rest-steps/api/BookshopApi.java` ```java package api; import net.serenitybdd.annotations.Step; import net.serenitybdd.rest.SerenityRest; import static net.serenitybdd.rest.SerenityRest.given; /** A step library: each @Step method is one line in the report, and holds the REST calls for it. */ public class BookshopApi { private String baseUri; public void usingTheServerAt(String baseUri) { this.baseUri = baseUri; } @Step("List the books in the catalogue") public void listTheBooks() { given().baseUri(baseUri).when().get("/api/books"); } @Step("Look up book number {0}") public void lookUpBook(int id) { given().baseUri(baseUri).when().get("/api/books/" + id); } @Step("Place an order for book {0} as a signed-in reader") public void placeAnOrderFor(int bookId) { given().baseUri(baseUri) .header("Authorization", "Bearer abc123") .contentType("application/json") .body("{\"items\":[{\"bookId\":" + bookId + ",\"qty\":2}],\"name\":\"Ada\"}") .when().post("/api/orders"); } @Step("Place an order without signing in") public void placeAnOrderWithoutSigningIn() { given().baseUri(baseUri) .contentType("application/json") .body("{\"items\":[{\"bookId\":1,\"qty\":1}],\"name\":\"Ada\"}") .when().post("/api/orders"); } @Step("The response should have status {0}") public void responseShouldHaveStatus(int status) { SerenityRest.then().statusCode(status); } @Step("The response should list {0} books") public void responseShouldListBooks(int count) { SerenityRest.then().body("size()", org.hamcrest.Matchers.equalTo(count)); } @Step("The response should show the title {0}") public void responseShouldShowTitle(String title) { SerenityRest.then().body("title", org.hamcrest.Matchers.equalTo(title)); } @Step("The response should show the error {0}") public void responseShouldShowError(String message) { SerenityRest.then().body("error", org.hamcrest.Matchers.equalTo(message)); } @Step("The order total should be {0}") public void orderTotalShouldBe(double total) { SerenityRest.then().body("total", org.hamcrest.Matchers.equalTo((float) total)); } } ``` ### `rest-steps/api/BookshopApiTest.java` ```java package api; import net.serenitybdd.annotations.Steps; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Browsing the book catalogue") class BookshopApiTest { static final BookshopServer server = new BookshopServer(); static int port; @Steps BookshopApi bookshop; @BeforeAll static void startServer() throws Exception { port = server.start(); } @AfterAll static void stopServer() { server.stop(); } @BeforeEach void pointAtTheServer() { bookshop.usingTheServerAt("http://localhost:" + port); } @Test @DisplayName("A shopper sees the catalogue and finds Clean Code by its number") void shopperFindsABook() { bookshop.listTheBooks(); bookshop.responseShouldHaveStatus(200); bookshop.responseShouldListBooks(2); bookshop.lookUpBook(2); bookshop.responseShouldShowTitle("Clean Code"); } @Test @DisplayName("A shopper who asks for a book that does not exist is told so") void unknownBook() { bookshop.lookUpBook(99); bookshop.responseShouldHaveStatus(404); bookshop.responseShouldShowError("Book not found"); } @Test @DisplayName("A signed-in reader can place an order, and an anonymous visitor cannot") void ordering() { bookshop.placeAnOrderFor(1); bookshop.responseShouldHaveStatus(201); bookshop.orderTotalShouldBe(90.0); bookshop.placeAnOrderWithoutSigningIn(); bookshop.responseShouldHaveStatus(401); bookshop.responseShouldShowError("Authentication required"); } } ``` ### `rest-steps/api/BookshopServer.java` ```java package api; import com.sun.net.httpserver.HttpServer; import java.io.IOException; import java.net.InetSocketAddress; import java.nio.charset.StandardCharsets; /** A tiny stand-in for the application under test, so the example runs without a network. */ public class BookshopServer { private HttpServer server; public int start() throws IOException { server = HttpServer.create(new InetSocketAddress("localhost", 0), 0); server.createContext("/api/books", exchange -> { String path = exchange.getRequestURI().getPath(); int status = 200; String body; if (path.equals("/api/books")) { body = "[{\"id\":1,\"title\":\"Fluent Python\",\"price\":45.0},{\"id\":2,\"title\":\"Clean Code\",\"price\":33.5}]"; } else if (path.equals("/api/books/2")) { body = "{\"id\":2,\"title\":\"Clean Code\",\"price\":33.5}"; } else { status = 404; body = "{\"error\":\"Book not found\"}"; } byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().add("Content-Type", "application/json"); exchange.sendResponseHeaders(status, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); }); server.createContext("/api/orders", exchange -> { boolean signedIn = "Bearer abc123".equals(exchange.getRequestHeaders().getFirst("Authorization")); int status = signedIn ? 201 : 401; String body = signedIn ? "{\"id\":\"ORD-1001\",\"total\":90.0,\"status\":\"PLACED\"}" : "{\"error\":\"Authentication required\"}"; byte[] bytes = body.getBytes(StandardCharsets.UTF_8); exchange.getResponseHeaders().add("Content-Type", "application/json"); exchange.sendResponseHeaders(status, bytes.length); exchange.getResponseBody().write(bytes); exchange.close(); }); server.start(); return server.getAddress().getPort(); } public void stop() { server.stop(0); } } ``` --- # Configuration: serenity.conf, Properties and Environments Verified against Serenity **5.4.1**. The behaviour described here was run against the example at the end of the page. ## 1. Where configuration comes from | Source | Where | Read? | |---|---|---| | `serenity.conf` (HOCON) | `src/test/resources/` (on the classpath) | yes | | `serenity.conf` | project root | **no, ignored** | | `serenity.properties` | project root | yes | | `serenity.properties` | `src/test/resources/` | yes | | System properties | `-Dkey=value` on the Maven command line | yes, override the files | What wins: - A `-D` option wins over both files (`-Dwebdriver.base.url=...` replaced the value in the file). - `serenity.properties` in the **project root** wins over `serenity.conf` in `src/test/resources`. - `serenity.conf` wins over a `serenity.properties` that is also in `src/test/resources`. - OS environment variables (`SERENITY_PROJECT_NAME=...`) did **not** set `serenity.project.name`. `thucydides.*` and `serenity.*` are synonyms: `thucydides.project.name = "x"` in either file sets the project name. Prefer `serenity.*`. After deleting or moving a configuration file, run `mvn clean ...`: Maven leaves the old copy in `target/test-classes`, so a deleted `serenity.conf` is still read (checked: the old value was still used without `clean`). ## 2. `serenity.conf` HOCON. Nested and dotted forms mean the same: ```hocon serenity { project.name = "My project" take.screenshots = FOR_FAILURES } webdriver { base.url = "https://example.com" } headless.mode = true ``` The full example, with environments, is `config/resources/serenity.conf`. ## 3. Environments ```hocon environments { default { app.url = "https://dev.example.com" db.name = "dev" } staging { app.url = "https://staging.example.com" } all { company = "Acme" } } ``` Pick one with `-Denvironment=staging`. - With no `environment`, the `default` block applies. With `-Denvironment=staging`, `staging` applies and values it does not define fall back to `default` (`db.name` stayed `dev`). - `all` applies in every environment. - **Your own keys inside `environments { }` are NOT visible as plain properties.** `environmentVariables.getProperty("app.url")` returns **null**, in every environment. Read them through `EnvironmentSpecificConfiguration` (next section). This is the usual cause of "I get null when I read a value from serenity.conf". - **Serenity's own keys inside `environments { }` do work** and override the top-level value: `webdriver.base.url` and `serenity.project.name` inside `environments { staging { } }` took effect with `-Denvironment=staging`. Values in `default` override the top-level value too. - Command-line override of a value in `environments`: use the full name, `-Denvironments.staging.app.url=https://...`. A plain `-Dapp.url=...` did **not** override it. ## 4. Reading values in code ```java EnvironmentVariables env = SystemEnvironmentVariables.currentEnvironmentVariables(); // net.thucydides.model.environment String name = ThucydidesSystemProperty.SERENITY_PROJECT_NAME.from(env); // net.thucydides.model String url = EnvironmentSpecificConfiguration.from(env).getOptionalProperty("app.url").orElse("none"); // net.serenitybdd.model.environment ``` - Works anywhere, including static code and static initialisers. `Serenity.environmentVariables()` (`net.serenitybdd.core.Serenity`) returns the same kind of object. - In a step library (a class with `@Step` methods used through `@Steps`), declare a field `EnvironmentVariables environmentVariables;` (type `net.thucydides.model.util.EnvironmentVariables`): Serenity injects it. - Built-in keys: `ThucydidesSystemProperty.X.from(env)` (also `integerFrom`, `booleanFrom`, `isDefinedIn`) or `env.getProperty("webdriver.base.url")`. - Your own keys outside `environments { }` can be read with `env.getProperty("my.key")`. ## 5. Common keys All of these exist in `ThucydidesSystemProperty` (checked). Meanings are from the Serenity documentation, except where marked *(run)*. | Key | Meaning | |---|---| | `webdriver.driver` | Which browser to run in (`chrome`, `firefox`, `edge`, `safari`, ...). An unknown name fails with `Unsupported browser type` *(run)*. | | `webdriver.base.url` | The default starting URL; base for relative paths. Read back *(run)*. | | `headless.mode` | Headless Chrome, Firefox or Edge. `true` added Chrome's `--headless` arguments *(run)*. | | `webdriver.chrome.driver` | Path to a chromedriver binary. Only set it if you really supply one; a missing file is an error *(run)*. | | `webdriver.autodownload` | `false` stops Serenity downloading driver binaries. | | `webdriver.remote.url`, `webdriver.remote.driver` | The URL of a Selenium Grid or cloud hub, and the driver to use there. | | `webdriver.timeouts.implicitlywait` | How long the driver waits for elements to appear, in milliseconds. | | `webdriver.wait.for.timeout` | How long fluent waits wait, in milliseconds. | | `serenity.project.name` | The name shown on reports. Read back *(run)*. | | `serenity.outputDirectory` | Where reports go (default `target/site/serenity`). | | `serenity.take.screenshots` | `FOR_EACH_ACTION`, `BEFORE_AND_AFTER_EACH_STEP`, `AFTER_EACH_STEP`, `FOR_FAILURES` or `DISABLED`. On a passing test `DISABLED` and `FOR_FAILURES` stored no screenshots; the other values, and not setting it, stored some *(run)*. | | `serenity.browser.width`, `serenity.browser.height` | Browser window size. | | `serenity.restart.browser.frequency` | Start a new browser every N rows in data-driven tests. | | `serenity.reports.show.step.details` | Show a breakdown of steps by result in report tables. | Do not invent keys. If a key is not in the Serenity documentation or `ThucydidesSystemProperty`, it is probably ignored without any message. ## 6. Setting values from the build - **Maven, one run:** `mvn verify -Denvironment=staging -Dwebdriver.driver=firefox`. - **Maven, fixed in the pom:** put them in the test plugin's ``. (A pom property such as `${cucumber.filter.tags}` can feed it.) - **Gradle:** `gradle test -Denvironment=staging` does **not** reach the tests unless `build.gradle` forwards it, as for the Cucumber options (see the Cucumber page): a filtered selection inside `test { }`, such as `systemProperties System.properties.findAll { k, v -> k.toString().startsWith('serenity.') || k == 'environment' }`. ## 7. Mistakes | Wrong | Right | |---|---| | `serenity.conf` in the project root | `src/test/resources/serenity.conf` | | Reading `app.url` with `env.getProperty` when it is defined under `environments { }` | `EnvironmentSpecificConfiguration.from(env).getOptionalProperty("app.url")` | | `-Dapp.url=...` to override a value defined under `environments { staging { } }` | `-Denvironments.staging.app.url=...` | | An old `serenity.properties` in the project root silently beating `serenity.conf` | Keep one file, or remember that the root properties file wins | | Setting a key through an OS environment variable | Use `-D`, the file, or the build tool's system property settings | | Hard-coding `webdriver.chrome.driver` to a path on one machine | Leave it unset (see Troubleshooting) | ## 8. Not covered Cloud and grid capabilities (`serenity.driver.capabilities`, `webdriver.capabilities.*`), proxies, Appium settings, the `drivers { os { } }` blocks, requirements and report customisation keys, and `.serenity` files in a user home directory. See the documentation page of Serenity properties. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). ### `config/cfg/ConfigReader.java` ```java package cfg; import net.serenitybdd.model.environment.EnvironmentSpecificConfiguration; import net.thucydides.model.util.EnvironmentVariables; /** A step library. Serenity injects the EnvironmentVariables field. */ public class ConfigReader { EnvironmentVariables environmentVariables; public String appUrl() { return EnvironmentSpecificConfiguration.from(environmentVariables).getOptionalProperty("app.url").orElse(""); } public boolean injected() { return environmentVariables != null; } } ``` ### `config/cfg/ConfigurationTest.java` ```java package cfg; import net.serenitybdd.annotations.Steps; import net.serenitybdd.core.Serenity; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.model.environment.EnvironmentSpecificConfiguration; import net.thucydides.model.ThucydidesSystemProperty; import net.thucydides.model.environment.SystemEnvironmentVariables; import net.thucydides.model.util.EnvironmentVariables; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class ConfigurationTest { @Steps ConfigReader reader; @Test void readsValuesFromSerenityConf() { // Anywhere in the code, including a static context: EnvironmentVariables env = SystemEnvironmentVariables.currentEnvironmentVariables(); EnvironmentSpecificConfiguration environmentConfig = EnvironmentSpecificConfiguration.from(env); // Plain keys: through the property enum, or by name System.out.println("CFG base.url=" + ThucydidesSystemProperty.WEBDRIVER_BASE_URL.from(env)); System.out.println("CFG project.name=" + ThucydidesSystemProperty.SERENITY_PROJECT_NAME.from(env)); System.out.println("CFG by-name=" + env.getProperty("webdriver.base.url")); System.out.println("CFG serenity-facade=" + Serenity.environmentVariables().getProperty("webdriver.base.url")); System.out.println("CFG my.setting=" + env.getProperty("my.setting")); // Keys under environments { }: ONLY through EnvironmentSpecificConfiguration System.out.println("CFG plain app.url=" + env.getProperty("app.url")); System.out.println("CFG specific app.url=" + environmentConfig.getOptionalProperty("app.url").orElse("")); System.out.println("CFG specific db.name=" + environmentConfig.getOptionalProperty("db.name").orElse("")); System.out.println("CFG specific company=" + environmentConfig.getOptionalProperty("company").orElse("")); System.out.println("CFG injected=" + reader.injected() + " " + reader.appUrl()); assertThat(ThucydidesSystemProperty.SERENITY_PROJECT_NAME.from(env)).isNotBlank(); assertThat(reader.injected()).isTrue(); } } ``` ### `config/resources/serenity.conf` ```hocon # src/test/resources/serenity.conf (HOCON). Serenity reads it from the classpath, not from the project root. serenity { project.name = "Config demo" take.screenshots = FOR_FAILURES } webdriver { base.url = "https://base.example.com" } headless.mode = true # A key of your own, outside environments { } my.setting = "plain value" # Values that depend on the environment. Select one with -Denvironment=staging. environments { default { app.url = "https://dev.example.com" db.name = "dev" } staging { app.url = "https://staging.example.com" } all { company = "Acme" } } ``` --- # Project Setup: Dependencies and Configuration Verified against Serenity **5.4.1**. The dependency sets, JUnit BOM and report plugin were run end to end (`mvn clean verify` ran the examples and produced the report). The `serenity.conf` snippet follows the project's own samples but was not run. Use one Serenity version for every `net.serenity-bdd` artifact (a `serenity.version` property). Java 17+. Playwright modules are published up to 5.3.11, so a Playwright project may need `serenity.version` 5.3.11 for those artifacts; check Maven Central for the latest before pinning. ## Dependencies by style (all `test` scope) | Style | `net.serenity-bdd` artifacts | Other | |---|---|---| | Page Objects + Selenium | `serenity-core`, `serenity-junit5` | `org.junit.jupiter:junit-jupiter`, `org.assertj:assertj-core` | | Page Objects + Playwright | `serenity-core`, `serenity-junit5`, `serenity-playwright` | + `com.microsoft.playwright:playwright` | | Screenplay + Selenium | `serenity-core`, `serenity-junit5`, `serenity-screenplay`, `serenity-screenplay-webdriver`, `serenity-ensure` | JUnit Jupiter, AssertJ | | Screenplay + Playwright | `serenity-core`, `serenity-junit5`, `serenity-screenplay`, `serenity-screenplay-playwright`, `serenity-playwright` | JUnit Jupiter, `com.microsoft.playwright:playwright` | | Screenplay + REST | `serenity-core`, `serenity-junit5`, `serenity-screenplay`, `serenity-screenplay-rest`, `serenity-rest-assured`, `serenity-ensure` | JUnit Jupiter | | Cucumber (+ Screenplay) | the Screenplay web set above, plus `serenity-cucumber` | `io.cucumber:cucumber-java`, `io.cucumber:cucumber-junit-platform-engine`, `org.junit.platform:junit-platform-suite` | Serenity 5.4.1 itself depends on **JUnit 6.1.3** (`serenity-junit5`) and **Cucumber 8.0.4** (`serenity-cucumber`). Don't pin older JUnit or Cucumber versions: mixed JUnit versions fail at run time with "conflicting versions" errors, even though the code compiles. Import the JUnit BOM so every JUnit artifact (including `junit-platform-suite` and the engine that `cucumber-junit-platform-engine` brings in) has the same version: ```xml org.junitjunit-bom6.1.3pomimport ``` Then list `junit-jupiter` and `junit-platform-suite` without versions, and use Cucumber 8.0.4 for `cucumber-java` and `cucumber-junit-platform-engine`. Other versions in the harness: Playwright 1.52.0, AssertJ 3.27.7. Add `org.slf4j:slf4j-simple` to silence logging warnings. ## Build and reports ```xml org.apache.maven.pluginsmaven-surefire-plugin3.5.1 org.apache.maven.pluginsmaven-failsafe-plugin3.5.1 net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} serenity-reportspost-integration-testaggregate ``` - Compile only: `mvn test-compile` (fast, use it to check generated code). - Run and build the report: `mvn clean verify`. The report is in `target/site/serenity/index.html`. - Name test classes so surefire/failsafe pick them up (`*Test`, or `*IT` for failsafe); a Cucumber runner is a JUnit Platform `@Suite` class. ## `src/test/resources/serenity.conf` (HOCON) ```hocon serenity { project.name = "My project" take.screenshots = FOR_FAILURES # FOR_EACH_ACTION, BEFORE_AND_AFTER_EACH_STEP, DISABLED } headless.mode = true webdriver.base.url = "https://example.com" environments { default { base.url = "https://example.com" } } ``` `serenity.*` and `thucydides.*` keys are synonyms; prefer `serenity.*`. Don't invent keys: check `docs/reference` on https://serenity-bdd.github.io or `ThucydidesSystemProperty` in `serenity-model`. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). --- # Troubleshooting by Symptom Verified against Serenity **5.4.1** (Playwright modules: **5.3.11**). Each entry was reproduced in a test project and its fix was run. How to use this page: find the message you see (the text is quoted from the real output), read the cause, apply the fix, and run the build again. If nothing here matches, read the first `Caused by:` and the first `[ERROR]` line in the Maven output, not the last. ## Where to look first - Serenity writes one JSON file per test to `target/site/serenity/` as the tests run. If the HTML report is missing, these files still show what happened. - The line `Results: N tests | ...` in the Maven output (from the Serenity Maven plugin) tells you how many tests Serenity saw. - `mvn -q test-compile` separates compile errors from run-time ones. ## The build fails or does nothing ### "The wrapped NoClassDefFoundError is likely caused by the versions of JUnit jars ... not being properly aligned" ``` The following conflicting versions were detected: - org.junit.jupiter.api: 6.1.3 - org.junit.platform.suite.api: 1.12.2 ... ``` - **Cause:** the pom pins `junit-jupiter`, `junit-platform-suite` or another JUnit artifact to a version other than the one Serenity brings. Serenity 5.4.1 depends on **JUnit 6.1.3**. The code compiles; the tests fail when they start. - **Fix:** remove the pinned JUnit versions and import the JUnit BOM so all JUnit artifacts match: ```xml org.junitjunit-bom6.1.3pomimport ``` List `junit-jupiter` and `junit-platform-suite` without versions. ### `java.lang.AbstractMethodError: Receiver class io.cucumber.junit.platform.engine.CucumberConfiguration does not define or inherit an implementation of the resolved method 'abstract ... getGlueDiscoveryRequest()'` - **Cause:** `cucumber-java` or `cucumber-junit-platform-engine` is pinned to a version that does not match the Cucumber that `serenity-cucumber` brings (8.0.4 for Serenity 5.4.1; 7.34.2 for 5.3.11). Reproduced with 7.31.0. - **Fix:** use the version Serenity brings (check with `mvn dependency:tree -Dincludes=io.cucumber`), or leave the Cucumber versions out. ### `Failed to transform configuration parameter with key 'cucumber.plugin' and initial value 'io.cucumber.core.plugin.SerenityReporterParallel,pretty'` - **Cause:** the Serenity plugin class is named with the wrong package. - **Fix:** `@ConfigurationParameter(key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,pretty")`. ### The build succeeds but `Results: 0 tests | 0 passed | ...`, no Cucumber scenarios ran, and the report is empty ``` [INFO] - Results: 0 tests | 0 passed | 0 failed | 0 errors | 0 compromised | 0 pending | 0 aborted ``` - **Cause:** Maven's test plugin only runs classes whose names match its default patterns (`*Test`, `*Tests`, `Test*`, `*TestCase`). A Cucumber `@Suite` class called, for example, `CucumberSuite` or `CucumberTestSuite` is skipped without any warning. Reproduced with `CucumberSuite`. - **Fix:** name the runner `RunCucumberTests` (or any name ending in `Test` or `Tests`), or add the name to `` in the surefire plugin configuration. ## The report is missing or wrong ### Tests failed, and there is no `target/site/serenity/index.html` - **Cause:** the report is built by the Serenity Maven plugin in the `post-integration-test` phase, which Maven never reaches when tests fail. The per-test JSON files exist, but no HTML. Reproduced: after a failing scenario, `mvn verify` printed `There are test failures`, and `index.html` was absent. - **Fix:** run the report goal after the failed build. This is the recommended way, because the build keeps its failing exit code: ``` mvn net.serenity-bdd.maven.plugins:serenity-maven-plugin:5.4.1:aggregate ``` (this produced `index.html`). The alternative is to let the build continue past test failures with `-Dmaven.test.failure.ignore=true`, but then the exit code is 0 even when tests fail, so a CI job must check results another way; avoid it unless you do. To find out what failed without any report, read the JSON files: `python3 scripts/results.py target/site/serenity` (see `test-results.md`). ### `Conflicting module versions. Module [groovy-xml is loaded in version 4.0.29 and you are trying to load version 3.0.9` / `Could not initialize class io.restassured.RestAssured` - **Cause:** the pom pins REST-assured (`io.rest-assured:*`) or Groovy (`org.codehaus.groovy:groovy*`, Groovy 3) versions left over from an older Serenity. Serenity 5.4.1's REST support uses Groovy 4. Reproduced with REST-assured 4.5.1 and Groovy 3.0.9. - **Fix:** delete those dependencies and let `serenity-rest-assured` bring its own. Seen when upgrading a real Serenity 2.4 project, which then passed 3 of 3 scenarios. ### The report shows more tests, or old failures, than the run had - **Cause:** Serenity counts every outcome file in `target/site/serenity`, including ones left by earlier runs. Reproduced: after a full run (5 scenarios), a run filtered to 1 scenario, without cleaning, reported `Results: 5 tests`; after the directory was deleted it reported 1 (the verification script in the serenity-core repository (`docs/ai/skill`), case `stale-outcomes-counted`). - **Fix:** start from a clean directory: `mvn clean verify`, or delete `target/site/serenity` (Gradle: `gradle clean test`, whose `clean` also clears it). ### "No BaseStepListener has been registered - are you running your test using the Serenity runners?" ``` java.lang.NullPointerException: No BaseStepListener has been registered - are you running your test using the Serenity runners? ``` - **Cause:** code that uses Serenity's reporting or Screenplay (for example `Serenity.recordReportData()`, an actor performing a task) runs outside a Serenity-managed test. In Cucumber the usual reason is that the Serenity plugin is not in the runner's `PLUGIN_PROPERTY_NAME`. Reproduced by removing the plugin. - **Fix:** in Cucumber, add `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel` to the plugins. In a JUnit 5 test, add `@ExtendWith(SerenityJUnit5Extension.class)`. ### A value defined in `serenity.conf` reads as `null` - **Cause (two common ones):** (1) the value is under `environments { }`, and `environmentVariables.getProperty("app.url")` never returns those; (2) the file is not where Serenity looks: `serenity.conf` is read from `src/test/resources`, not the project root. Reproduced both (the verification script in the serenity-core repository (`docs/ai/skill`)). - **Fix:** (1) `EnvironmentSpecificConfiguration.from(env).getOptionalProperty("app.url")`; (2) move the file to `src/test/resources/serenity.conf` and run `mvn clean`. ### Filtering by tag does nothing under Gradle: all scenarios still run - **Cause:** Gradle does not pass `-D` options on the command line to the tests' JVM, so `gradle test -Dcucumber.filter.tags=@smoke` is ignored. Reproduced: 4 outcomes with the option, unchanged, and 1 once the build forwarded it. - **Fix:** forward the option in `build.gradle`, as in the Cucumber page: `systemProperties System.properties.findAll { k, v -> k.toString().startsWith('cucumber.') }` inside `test { }`. ## The browser ### `NullPointerException: Cannot invoke "org.openqa.selenium.WebDriver.get(String)" because "this.driver" is null` - **Cause:** a `@Managed WebDriver driver;` field in a **step library** (or any class other than the test class). `@Managed` only fills the field in the test class; elsewhere it stays null. Reproduced in a step library. - **Fix:** extend `net.serenitybdd.core.steps.UIInteractionSteps` and use `getDriver()` (or use a page object, which gets the driver itself). Keep `@Managed` for the test class. ### `DriverConfigurationError: Unsupported browser type: chromee. Did you mean firefox?` - **Cause:** `webdriver.driver` (system property or `serenity.conf`) names a browser Serenity does not know. Reproduced with a misspelling. - **Fix:** use a supported name: `chrome`, `firefox`, `edge`, `safari`, `iexplorer`, `appium`, `remote`, ... (in this case the message guessed `firefox` for a misspelt `chrome`, so read the list rather than trusting the suggestion). ### `DriverConfigurationError: After several attempts, could not instantiate new WebDriver instance of type class org.openqa.selenium.chrome.ChromeDriver (Unable to obtain: chromedriver, error chromedriver must exist: /no/such/chromedriver` - **Cause:** `webdriver.chrome.driver` points to a file that does not exist (a stale path from another machine or an old setup). - **Also seen as:** `chromedriver must be executable: src/test/resources/webdriver/mac/chromedriver`, from an old `drivers { mac { webdriver.chrome.driver = "..." } }` block with a bundled binary (found upgrading a real Serenity 2.1 project; removing the block let Chrome start). - **Fix:** remove the property so Selenium downloads or finds a matching driver by itself, or set it to a real path. Do not commit machine-specific absolute paths; if a driver must be supplied, see the OS-specific `drivers` settings in the driver configuration docs. ### `no such element: Unable to locate element: {"method":"xpath","selector":"input.new-todo"}` (Screenplay with Selenium) - **Cause:** `Target.the(..).locatedBy("input.new-todo")`. Serenity reads the string as XPath when it compiles as an XPath expression, and `input.new-todo` does (a name test). So the element is searched as XPath and not found. `.new-todo`, `#id` and `[data-x=y]` are read as CSS. - **Fix:** `Target.the(..).located(By.cssSelector("input.new-todo"))`, or a CSS selector that starts with `.`, `#` or `[`. ### `NoClassDefFoundError: io/cucumber/core/eventbus/EventBus` (and similar) after raising the Serenity version - **Cause:** the project still pins an older Cucumber (4.x) and excludes `cucumber-core` from Serenity, as old starter projects did. Seen upgrading a real Serenity 2.1 project; not reproduced in the test projects here. After the pin and the exclusion were replaced by Cucumber 8.0.4 the project built; remember `mvn clean`, because old compiled classes can hide the next error (see the upgrading reference). - **Fix:** use the Cucumber version Serenity brings (8.0.4 for 5.4.1) and remove the exclusion. ## Not reproduced here These come up often in public questions. They are not covered by a tested fix, so use the driver configuration and Maven documentation, and do not rely on advice you cannot check: - `Chrome failed to start: exited normally` / `session not created` after a Chrome update, `DevToolsActivePort file doesn't exist`, and browser start-up problems inside Docker or on Jenkins (they depend on the machine, user and container). - Dependency convergence errors and `Could not resolve dependencies` (they depend on the project's other dependencies). - `NoClassDefFoundError` after upgrading from Serenity 3 to 4 (for example `net.thucydides.model.util.EnvironmentVariables`): classes moved between packages. The upgrade guide in the documentation lists them. - Slow report aggregation, wrong counts and reports from parallel runs. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). --- # Upgrading to Serenity 5 and Recognising Old Code Verified against Serenity **5.4.1**. The class counts come from the released jars, and the recipe was applied to five real projects. ## 1. What changed, by release | Coming from | Java needed | What you must change | |---|---|---| | 4.x (tested: 4.2.34) | Java **17** (4.x classes are Java 11, 5.x classes are Java 17) | Versions only. All 1,587 public classes in the core, model, JUnit, Screenplay (including WebDriver and REST), REST-assured, Cucumber and Ensure jars have the same name and package in 5.4.1. A 4.2.8 project compiled with only a version bump. Align the JUnit and Cucumber versions with Serenity's (section 3). | | 3.x (tested: 3.9.8) | Java 17 (3.x classes are Java 8) | Version bump, **511 of 1,574 classes moved** (section 2), the artifact changes in section 3, and the fixes for removed API found by the compiler. | | 2.x | Java 17 | The same as 3.x, plus renamed Cucumber artifacts. | Serenity 5.4.1 depends on **JUnit 6.1.3** and **Cucumber 8.0.4** (`../references/project-setup.md`). ## 2. Classes that moved (3.x to 5.x) Of the 1,574 public classes in the 3.9.8 jars: 1,051 are unchanged, 511 moved to exactly one new place, 7 have a same-named class in more than one place, 5 are gone. Many `net.thucydides` packages are still correct (for example `net.thucydides.core.steps.ScenarioSteps`, `net.thucydides.core.pages.Pages`, and the 47 classes in `net.thucydides.core.steps` that did not move): only the classes in the table moved. All of `net.serenitybdd.screenplay.*`, `net.serenitybdd.core.pages`, `net.serenitybdd.junit.runners` and `net.thucydides.core.webdriver` (except two classes) kept their packages. | Old package (3.x) | New package | Classes | |---|---|---| | `net.serenitybdd.core` | `net.serenitybdd.model` | `CurrentOS`, `IgnoredStepException`, `PendingStepException`, `SerenitySystemProperties`, `SkipStepException` | | `net.serenitybdd.core.buildinfo` | `net.serenitybdd.model.buildinfo` | `BuildInfoProvider`, `BuildProperties`, `DriverCapabilityRecord`, `PropertyBasedDriverCapabilityRecord` | | `net.serenitybdd.core.collect` | `net.serenitybdd.model.collect` | `NewList`, `NewMap`, `NewSet` | | `net.serenitybdd.core.di` | `net.serenitybdd.model.di` | `DependencyInjector`, `ModelInfrastructure` | | `net.serenitybdd.core.environment` | `net.serenitybdd.model.environment` | `ConfiguredEnvironment`, `EnvironmentSpecificConfiguration`, `UndefinedEnvironmentVariableException`, `UnknownEnvironmentException` | | `net.serenitybdd.core.exceptions` | `net.serenitybdd.model.exceptions` | `CausesAssertionFailure`, `CausesCompromisedTestFailure`, `NoException`, `SerenityManagedException`, `TestCompromisedException`, `TheErrorType`, `UnrecognisedException` | | `net.serenitybdd.core.history` | `net.serenitybdd.model.history` | `ClearDirectoryContents`, `FileSystemTestOutcomeSummaryRecorder`, `HistoricalFlagProvider`, `LeaveDirectoryContents`, `NewFailure`, `PrepareHistoryDirectory`, `PreviousTestOutcome`, `TestOutcomeSummaryRecorder` | | `net.serenitybdd.core.i8n` | `net.serenitybdd.model.i8n` | `LocalisedLabels` | | `net.serenitybdd.core.model` | `net.serenitybdd.model.model` | `FailureDetails`, `TestOutcomeName` | | `net.serenitybdd.core.rest` | `net.serenitybdd.model.rest` | `RestMethod`, `RestQuery` | | `net.serenitybdd.core.strings` | `net.serenitybdd.model.strings` | `FirstLine`, `Joiner` | | `net.serenitybdd.core.tags` | `net.serenitybdd.model.tags` | `EnvironmentDefinedTags` | | `net.serenitybdd.core.time` | `net.serenitybdd.model.time` | `InternalSystemClock`, `Stopwatch`, `SystemClock` | | `net.serenitybdd.core.webdriver.driverproviders` | `net.serenitybdd.core.webdriver.servicepools` | `DriverServices` | | `net.thucydides.core` | `net.thucydides.model` | `ThucydidesSystemProperty` | | `net.thucydides.core.adapters` | `net.thucydides.model.adapters` | `JUnitStrategy`, `MultiStrategyAdapter`, `TestFramework`, `TestStrategyAdapter` | | `net.thucydides.core.adapters.legacy` | `net.thucydides.model.adapters.legacy` | `DefaultJUnitAdapter`, `DefaultJUnitStrategy` | | `net.thucydides.core.annotations` | `net.serenitybdd.annotations` | `AnnotatedDescription`, `At`, `BlurScreenshots`, `ClearCookiesPolicy`, `DefaultUrl`, `Description`, `DriverOptions`, `Feature`, `Fields`, `Issue`, `Issues`, `Managed`, `ManagedPages`, `Manual`, `NamedUrl`, `NamedUrls`, `Narrative`, `Pending`, `Reported`, `Screenshots`, `SectionNumber`, `Shared`, `SingleBrowser`, `Step`, `StepGroup`, `Steps`, `TestAnnotations`, `TestsRequirement`, `TestsRequirements`, `Title`, `UsePersistantStepLibraries`, `UserStoryCode`, `Version`, `WhenPageOpens`, `WithDriver`, `WithTag`, `WithTagValuesOf`, `WithTags` | | `net.thucydides.core.batches` | `net.thucydides.model.batches` | `BatchManager`, `BatchStrategy`, `SystemVariableBasedBatchManager`, `TestCountBasedBatchManager`, `UnsupportedBatchStrategyException` | | `net.thucydides.core.configuration` | `net.thucydides.model.configuration` | `BuildDirectoryProvider`, `FilePathParser`, `MavenBuildDirectory`, `MavenOrGradleBuildPath`, `SessionLocalTempDirectory`, `SystemPropertiesConfiguration`, `TimeoutConfiguration`, `TimeoutValue` | | `net.thucydides.core.digest` | `net.thucydides.model.digest` | `Digest` | | `net.thucydides.core.environment` | `net.thucydides.model.environment` | `MockEnvironmentVariables`, `SystemEnvironmentVariables`, `TestLocalEnvironmentVariables` | | `net.thucydides.core.files` | `net.thucydides.model.files` | `TheDirectoryStructure` | | `net.thucydides.core.images` | `net.thucydides.model.images` | `Refit`, `ResizableImage`, `ResizedImage` | | `net.thucydides.core.issues` | `net.thucydides.model.issues` | `IssueKeyFormat`, `IssueTracking`, `SystemPropertiesIssueTracking` | | `net.thucydides.core.logging` | `net.thucydides.model.logging` | `ConsoleColors`, `ConsoleEvent`, `ConsoleHeading`, `ConsoleHeadingStyle`, `ConsoleLoggingListener`, `LoggingLevel` | | `net.thucydides.core.matchers` | `net.thucydides.model.matchers` | `BeanCollectionMatcher`, `BeanCountMatcher`, `BeanFieldMatcher`, `BeanMatcher`, `BeanMatcherAsserts`, `BeanMatchers`, `BeanPropertyMatcher`, `BeanUniquenessMatcher`, `BigDecimalValueMatcher`, `FileMatchers`, `InstantiatedBeanMatcher`, `MaxFieldValueMatcher`, `MinFieldValueMatcher`, `PublicThucydidesMatchers`, `ScreenshotHasFilenamesMatcher`, `SimpleValueMatcher`, `TestOutcomeResultsMatcher`, `ThucydidesMatchers` | | `net.thucydides.core.matchers.dates` | `net.thucydides.model.matchers.dates` | `BeanFields`, `DateCollectionContainsSameDatesMatcher`, `DateComparator`, `DateIsAfterMatcher`, `DateIsBeforeMatcher`, `DateIsBetweenMatcher`, `DateIsSameAsMatcher`, `DateMatcherFormatter`, `DateMatchers`, `DateTimeCollectionContainsSameDatesMatcher`, `DateTimeIsAfterMatcher`, `DateTimeIsBeforeMatcher`, `DateTimeIsBetweenMatcher`, `DateTimeIsCloseToAsMatcher`, `DateTimeIsSameAsMatcher`, `DateTimeIsSameTimeAsMatcher`, `TimeIsCloseToAsMatcher` | | `net.thucydides.core.model` | `net.thucydides.model.domain` | `AnnotatedStoryTitle`, `BadgeBackground`, `ContextIcon`, `DataSet`, `DataSetDescriptor`, `DataTable`, `DataTableRow`, `Downloadables`, `ErrorMessageFormatter`, `ExternalLink`, `FeatureTagAsDefined`, `HeuristicTestResult`, `LastElement`, `ManualTestEvidence`, `NumericalFormatter`, `OutcomeCounter`, `PackageBasedLeafRequirements`, `PathElement`, `PathElementResolver`, `ReportData`, `ReportNamer`, `ReportType`, `RequirementCache`, `Rule`, `RuleBackground`, `ScenarioOutcomeCounter`, `Stories`, `TakeScreenshots`, `TestCaseOrder`, `TestClassHierarchy`, `TestDuration`, `TestFailureException`, `TestOutcome`, `TestOutcomeAnnotationReader`, `TestOutcomeSummary`, `TestResult`, `TestResultComparison`, `TestResultList`, `TestStep`, `TestTag`, `TestTagCache`, `TestTags`, `TestType`, `TitleBuilder` | | `net.thucydides.core.model.failures` | `net.thucydides.model.domain.failures` | `FailureAnalysis`, `FailureAnalysisConfiguration`, `RootCause` | | `net.thucydides.core.model.features` | `net.thucydides.model.domain.features` | `ApplicationFeature` | | `net.thucydides.core.model.featuretags` | `net.thucydides.model.domain.featuretags` | `FeatureFileStrategy`, `FeatureTagStrategy`, `FeatureTitle`, `NoFeatureStrategy`, `StoryFileStrategy` | | `net.thucydides.core.model.flags` | `net.thucydides.model.domain.flags` | `Flag`, `FlagCounts`, `FlagProvider` | | `net.thucydides.core.model.formatters` | `net.thucydides.model.domain.formatters` | `IssueExtractor`, `ReportFormatter`, `TestCoverageFormatter` | | `net.thucydides.core.model.results` | `net.thucydides.model.domain.results` | `MergeStepResultStrategy`, `NextStepCompromisedStrategy`, `NextStepFailedStrategy`, `NextStepStatusTakesPriorityStrategy`, `NextStepWasSkippedStrategy`, `StepResultMergeStragegy` | | `net.thucydides.core.model.screenshots` | `net.thucydides.model.domain.screenshots` | `Screenshot`, `ScreenshotPreferencesByClass` | | `net.thucydides.core.model.stacktrace` | `net.thucydides.model.domain.stacktrace` | `FailureCause`, `RootCauseAnalyzer`, `StackTraceSanitizer` | | `net.thucydides.core.output` | `net.thucydides.model.output` | `ResultsOutput`, `SpreadsheetResultsOutput` | | `net.thucydides.core.reflection` | `net.thucydides.model.reflection` | `ClassFinder`, `FieldFinder`, `FieldSetter`, `FieldValue`, `MethodFinder`, `StackTraceAnalyser` | | `net.thucydides.core.releases` | `net.thucydides.model.releases` | `ReleaseManager` | | `net.thucydides.core.reports` | `net.thucydides.model.reports` | `AcceptanceTestFullReporter`, `AcceptanceTestLoader`, `AcceptanceTestReporter`, `AnsiEscapes`, `AsciiColors`, `FlagsAugmenter`, `FormatConfiguration`, `JiraUpdaterService`, `NumberOfThreads`, `OutcomeAugmenter`, `OutcomeFormat`, `OutcomeSummary`, `ReportGenerationFailedError`, `ReportLoadingFailedError`, `ReportOptions`, `ReportService`, `ReporterRuntime`, `ResultChecker`, `SystemReporterRuntime`, `TestOutcomeCounter`, `TestOutcomeLoader`, `TestOutcomeStream`, `TestOutcomes`, `TestOutcomesCompromised`, `TestOutcomesError`, `TestOutcomesFailures`, `ThucydidesReporter`, `UserStoryTestReporter` | | `net.thucydides.core.reports.adaptors` | `net.thucydides.model.reports.adaptors` | `AdaptorService`, `TestOutcomeAdaptor`, `UnknownAdaptor` | | `net.thucydides.core.reports.adaptors.common` | `net.thucydides.model.reports.adaptors.common` | `FilebasedOutcomeAdaptor` | | `net.thucydides.core.reports.adaptors.lettuce` | `net.thucydides.model.reports.adaptors.lettuce` | `LettuceXUnitAdaptor` | | `net.thucydides.core.reports.adaptors.specflow` | `net.thucydides.model.reports.adaptors.specflow` | `ScenarioSplitter`, `ScenarioStep`, `ScenarioStepReader`, `SpecflowAdaptor`, `SpecflowScenario`, `SpecflowScenarioTitleLine`, `SpecflowTableRow` | | `net.thucydides.core.reports.adaptors.xunit` | `net.thucydides.model.reports.adaptors.xunit` | `BasicXUnitLoader`, `CouldNotReadXUnitFileException`, `DefaultXUnitAdaptor`, `XUnitLoader` | | `net.thucydides.core.reports.adaptors.xunit.io` | `net.thucydides.model.reports.adaptors.xunit.io` | `XUnitFiles` | | `net.thucydides.core.reports.adaptors.xunit.model` | `net.thucydides.model.reports.adaptors.xunit.model` | `Skip`, `TestCase`, `TestError`, `TestException`, `TestSuite` | | `net.thucydides.core.reports.csv` | `net.thucydides.model.reports.csv` | `CSVReporter` | | `net.thucydides.core.reports.history` | `net.thucydides.model.reports.history` | `DateProvider`, `ProgressSnapshot`, `SystemDateProvider`, `TestResultSnapshot` | | `net.thucydides.core.reports.html` | `net.thucydides.model.reports.html` | `ContextIconFormatter`, `CucumberCompatibleFilter`, `CucumberTagConverter`, `DescriptionSplitter`, `ExampleTable`, `MarkdownRendering`, `ReportNameProvider`, `RequirementsFilter`, `ResultCounts`, `ResultIconFormatter`, `ScenarioResultCounts`, `TagExclusions`, `TagFilter`, `TagMatch`, `TestOutcomeFilter` | | `net.thucydides.core.reports.integration` | `net.thucydides.model.reports.integration` | `TestStepFactory` | | `net.thucydides.core.reports.json` | `net.thucydides.model.reports.json` | `AScenarioHasNoNameException`, `JSONConverter`, `JSONTestOutcomeReporter` | | `net.thucydides.core.reports.json.gson` | `net.thucydides.model.reports.json.gson` | `ClassTypeAdapter`, `CollectionAdapter`, `FileDeserializer`, `FileSerializer`, `GsonJSONConverter`, `GsonPreviousOutcomeConverter`, `InterfaceAdapter`, `OptionalTypeAdapter`, `StackTraceElementDeserializer`, `StackTraceElementSerializer`, `ZonedDateTimeAdapter` | | `net.thucydides.core.reports.junit` | `net.thucydides.model.reports.junit` | `JUnitXMLConverter`, `JUnitXMLOutcomeReporter` | | `net.thucydides.core.reports.matchers` | `net.thucydides.model.reports.matchers` | `TestOutcomeMatchers` | | `net.thucydides.core.reports.remoteTesting` | `net.thucydides.model.reports.remoteTesting` | `BrowserStackLinkGenerator`, `LinkGenerator`, `RemoteTestingLinkManager` | | `net.thucydides.core.reports.templates` | `net.thucydides.model.reports.templates` | `FreeMarkerTemplateManager`, `FreemarkerReportTemplate`, `ReportTemplate`, `TemplateManager`, `TemplateMergeException` | | `net.thucydides.core.reports.util` | `net.thucydides.model.reports.util` | `CopyDirectory` | | `net.thucydides.core.reports.xml` | `net.thucydides.model.reports.xml` | `NotAThucydidesReportException`, `TestOutcomeConverter` | | `net.thucydides.core.requirements` | `net.thucydides.model.requirements` | `AbstractRequirementsTagProvider`, `AllRequirements`, `BaseRequirementsService`, `ChildrenFirstOrderedMap`, `ClasspathRequirementsProviderService`, `CoreTagProvider`, `CouldNotLoadRequirementsException`, `DefaultCapabilityTypes`, `DefaultRequirements`, `DisabledRequirementsStore`, `ExcludedUnrelatedRequirementTypes`, `FeatureFilePath`, `FileSystemRequirements`, `FileSystemRequirementsService`, `FileSystemRequirementsStore`, `FileSystemRequirementsTagProvider`, `JSONRequirementsTree`, `MultiSourceRequirementsService`, `OverridableTagProvider`, `PackageAnnotationBasedTagProvider`, `PackageRequirementsTagProvider`, `ParentRequirementProvider`, `ParentRequirementsProvided`, `PathStartsWith`, `ReleaseProvider`, `RequirementAncestry`, `RequirementPersister`, `RequirementTypesProvider`, `Requirements`, `RequirementsList`, `RequirementsMerger`, `RequirementsPath`, `RequirementsProvided`, `RequirementsProviderService`, `RequirementsService`, `RequirementsStore`, `RequirementsTagProvider`, `RequirementsTree`, `RootDirectory`, `SearchForFilesOfType`, `SearchForFilesWithName`, `SerenityTestCaseFinder`, `TestOutcomeRequirementsTagProvider`, `WindowsFriendly` | | `net.thucydides.core.requirements.annotations` | `net.thucydides.model.requirements.annotations` | `ClassInfoAnnotations`, `NarrativeFinder` | | `net.thucydides.core.requirements.classpath` | `net.thucydides.model.requirements.classpath` | `AnnotatedTags`, `ChildElementAdder`, `ClassNarrative`, `LeafRequirementAdder`, `NarrativeTitle`, `NonLeafRequirementsAdder`, `PackageInfoClass`, `PackageInfoNarrative` | | `net.thucydides.core.requirements.model` | `net.thucydides.model.requirements.model` | `CustomFieldValue`, `Example`, `ExampleBuilder`, `FeatureBackgroundNarrative`, `FeatureType`, `LoadedNarrative`, `NarrativeReader`, `OverviewReader`, `Requirement`, `RequirementBuilderNameStep`, `RequirementBuilderTypeStep`, `RequirementDefinition`, `RequirementTree`, `RequirementTypeAt`, `RequirementsConfiguration` | | `net.thucydides.core.requirements.model.cucumber` | `net.thucydides.model.requirements.model.cucumber` | `AnnotatedFeature`, `BlankRowResultIcon`, `CucumberParser`, `DescriptionWithScenarioReferences`, `ExampleRowResultIcon`, `ExampleTableInMarkdown`, `FeatureFileAnaysisErrors`, `FeatureFileChecker`, `FeatureFileFinder`, `IdentifiedExampleTable`, `IdentifiedScenario`, `InvalidFeatureFileException`, `NamedExampleTable`, `NamedScenario`, `NarrativeFromCucumberComments`, `ReferencedExampleTable`, `ReferencedScenario`, `RenderedExampleTable`, `RowResultIcon`, `ScenarioDisplayOption`, `ScenarioReport`, `SummaryRowResultIcon`, `URIResource`, `UnknownExampleTable`, `UnknownScenario` | | `net.thucydides.core.requirements.reports` | `net.thucydides.model.requirements.reports` | `AcceptanceCriteriaRequirmentCounter`, `ChildRequirementCounter`, `CompoundDuration`, `ExampleOutcome`, `ExampleOutcomes`, `FileSystemRequirmentsOutcomeFactory`, `MultipleSourceRequirmentsOutcomeFactory`, `PercentageRequirementCounter`, `RenderMarkdown`, `ReportBadges`, `RequirementOutcome`, `RequirementsOutcomeFactory`, `RequirementsOutcomes`, `RequirementsOutcomesOfTypeCache`, `RequirementsPercentageFormatter`, `RequirementsProportionCounter`, `RequirmentCalculator`, `ScenarioOutcome`, `ScenarioOutcomes`, `ScenarioSummaryOutcome`, `SingleScenarioOutcome`, `SubrequirementsCount`, `SubrequirementsPercentageCount`, `SubrequirementsProportionCount` | | `net.thucydides.core.requirements.reports.cucumber` | `net.thucydides.model.requirements.reports.cucumber` | `FeatureCache`, `FeatureFileScenarioOutcomes`, `OptionalAnnotatedFeature`, `RenderCucumber` | | `net.thucydides.core.requirements.tree` | `net.thucydides.model.requirements.tree` | `Node` | | `net.thucydides.core.resources` | `net.thucydides.model.resources` | `FileResources`, `ResourceCopyingError`, `ResourceList` | | `net.thucydides.core.screenshots` | `net.serenitybdd.annotations` | `BlurLevel` | | `net.thucydides.core.screenshots` | `net.thucydides.model.screenshots` | `ScreenshotAndHtmlSource`, `ScreenshotException` | | `net.thucydides.core.statistics` | `net.thucydides.model.statistics` | `AtomicTestCount`, `TestCount` | | `net.thucydides.core.statistics.service` | `net.thucydides.model.statistics.service` | `AnnotationBasedTagProvider`, `ClasspathTagProviderService`, `ContextTagProvider`, `FeatureStoryTagProvider`, `InjectedTagProvider`, `JUnitTagProviderStrategy`, `TagProvider`, `TagProviderFilter`, `TagProviderService`, `TagProviderStrategy` | | `net.thucydides.core.steps` | `net.thucydides.model.steps` | `AnnotatedStepDescription`, `ExecutedStepDescription`, `FailingManualTestException`, `MetaField`, `ReplaceField`, `ScreenplayInspector`, `StepArgumentWriter`, `StepFailure`, `StepFailureException`, `StepListener`, `TestFailureCause`, `TestSourceType`, `TestStatus` | | `net.thucydides.core.steps.di` | `net.thucydides.model.steps.di` | `ClasspathDependencyInjectorService`, `DependencyInjectorService` | | `net.thucydides.core.steps.events` | `net.serenitybdd.cucumber.events` | `SetTestManualEvent` | | `net.thucydides.core.tags` | `net.thucydides.model.tags` | `BreadcrumbTagFilter`, `OutcomeTagFilter`, `TagConverters`, `TagScanner`, `Taggable` | | `net.thucydides.core.util` | `net.thucydides.model.util` | `Acronym`, `ConfigCache`, `EnvironmentVariables`, `FileSystemUtils`, `Inflection`, `Inflector`, `LocalPreferences`, `MultipleInflection`, `NameConverter`, `PropertiesLocalPreferences`, `PropertiesUtil`, `TagInflector`, `TestResources`, `VersionProvider` | | `net.thucydides.core.webdriver` | `net.thucydides.model.webdriver` | `Configuration`, `WebdriverAssertionError` | The annotations are the ones most code touches: `Step`, `Steps`, `Managed`, `DefaultUrl`, `Title`, `Narrative`, `Pending`, `WithTag`, `Issue` and the rest of `net.thucydides.core.annotations` are now in **`net.serenitybdd.annotations`**. `EnvironmentVariables` is in `net.thucydides.model.util`; `EnvironmentSpecificConfiguration` is in `net.serenitybdd.model.environment`. Ambiguous: `Story` (`net.serenitybdd.annotations.Story` for the annotation, `net.thucydides.model.domain.Story` for the model class), `CastMember` (`net.serenitybdd.screenplay.annotations.CastMember` for Screenplay), `Release`, `PathElements` and `TagParser`: decide by what the code uses it for. Gone: the WebDriverManager integration classes (`WebDriverManagerSetup`, `WebDriverManagerEnhancer`, `DefaultWebDriverManagerEnhancer`; drivers are found by Selenium itself now), `MergedRequirementList` and `RecordStepResultEvent`. **Rewriting imports automatically:** the `fix_imports.py` script in the serenity-core repository (`docs/ai/skill/upgrade-checks`) rewrites every class in the table (imports, fully qualified names, wildcard imports) and lists the ambiguous and removed classes it found, for a person to decide. ## 3. The upgrade recipe Each step was needed by at least one of the five tested projects. 1. **Java 17** or later for the build and the tests. 2. **Set the Serenity version** everywhere it appears (`serenity.version`, the Maven plugin version, `serenity.cucumber.version`). 3. **Artifact names.** `serenity-cucumber4`, `serenity-cucumber5` and `serenity-cucumber6` stopped at 2.1.2, 2.2.6 and 2.6.0: use **`serenity-cucumber`**. The JUnit 4 runner (`net.serenitybdd.junit.runners.SerenityRunner`) lives in **`serenity-junit`**, which a 3.x project got through `serenity-core`: add it, with JUnit 4.13.2, if you keep JUnit 4 tests. `serenity-jbehave` has no 5.x release (its last version is 1.46.0): JBehave stories need migrating to Cucumber first. Check any other `serenity-*` artifact on Maven Central before relying on it. 4. **Remove versions that Serenity manages.** Pins that were right for the old Serenity break the new one at run time, not at compile time: - Cucumber: a pinned `cucumber-java`, `cucumber-junit` or an `` of `cucumber-core` gives `NoClassDefFoundError: io/cucumber/core/eventbus/EventBus` (a project with Cucumber 4.8.1 and the exclusion). Use 8.0.4, or none, and delete the exclusion. - REST-assured and Groovy: pinned `io.rest-assured:*` and `org.codehaus.groovy:groovy*` (Groovy 3) give `Conflicting module versions. Module [groovy-xml is loaded in version 3.0.6 and you are trying to load version 4.0.29`. Delete them; `serenity-rest-assured` brings the right ones. - JUnit: see the troubleshooting reference ("conflicting versions"); import the JUnit BOM. 5. **Imports.** Run `fix_imports.py` (section 2). 6. **Configuration clean-up** (`../references/configuration.md`): - Delete a `drivers { mac { webdriver.chrome.driver = "src/test/resources/webdriver/..." } }` block of bundled driver binaries. A 2020 chromedriver cannot drive a current Chrome, and the symptom is `Could not instantiate class org.openqa.selenium.chrome.ChromeDriver ... chromedriver must be executable`. With the block removed, Chrome started. - `screenshooter=shutterbug` needs the separate `serenity-shutterbug` artifact; otherwise the test fails with `Unable to instantiate screen shooter net.serenitybdd.screenshots.shutterbug.ShutterbugScreenShooter`. Delete the line (default screenshots) or add the artifact. 7. **Compile from clean:** `mvn clean test-compile`. Without `clean`, Maven keeps classes compiled against the old libraries: a `cucumber.api.CucumberOptions` import compiled for as long as the stale class file existed, and the runner then silently ran **0 scenarios**. 8. **Fix removed or renamed API** (the compiler lists it). Found in the tested projects: `InputField.called("x")` and `PageElement.called("x")` (gone by 3.9.8) became `InputField.withNameOrId("x")` and `PageElement.withNameOrId("x")`. `cucumber.api.CucumberOptions` became `io.cucumber.junit.CucumberOptions`, and `cucumber.api.java.en.*` became `io.cucumber.java.en.*` (the `cucumber.api` package does not exist in Cucumber 8). 9. **Run the tests.** Failures that remain may belong to the application under test: three of the five old projects' remaining failures were a changed page (a search box and a to-do input that no longer exist). ## 4. Evidence: five real projects | Project (Serenity version) | Result | |---|---| | REST starter with Cucumber (2.4.51) | Version, `serenity-cucumber6` to `serenity-cucumber`, 2 class references rewritten, REST-assured and Groovy pins removed: **3 of 3 scenarios pass**. | | Cucumber 4 starter (2.1.12) | Version, 4 class references rewritten, Cucumber 4.8.1 and the exclusion replaced by 8.0.4, runner import fixed, bundled drivers removed: compiles and the 4 scenarios run in Chrome; they fail on the DuckDuckGo search box, which has changed. | | Screenplay with JUnit 4 and Cucumber, todo app (3.3.0) | Version, `serenity-junit` added, 6 class references rewritten: compiles; 4 of 10 scenarios pass, 6 fail on a to-do input that one of the sample apps no longer has. | | Screenplay quickstart (3.0.2) | Compiles after `called` became `withNameOrId`; the run needed the `shutterbug` line removed, then fails on Wikipedia's changed search box (`element not interactable`). | | Toolshop (4.2.8) | Compiles with only the version bump. Its tests were not run. | ## 5. JUnit 4 and the old Cucumber runner still work You do not have to move to JUnit 5. With `serenity-junit` and JUnit 4.13.2, `@RunWith(SerenityRunner.class)` with `@Steps` ran on 5.4.1 (1 test passed), and so did `@RunWith(CucumberWithSerenity.class)` with `@CucumberOptions(features = ..., glue = ...)` from `io.cucumber.junit` (3 scenarios passed). Use the JUnit 5 extension and the JUnit Platform suite for new code (`../references/cucumber.md`); do not rewrite a working JUnit 4 suite only to follow it. Keep one style per project. ## 6. Recognising old code in generated or copied examples | Old | Current | |---|---| | `import net.thucydides.core.annotations.{Step,Steps,Managed,DefaultUrl,...}` | `net.serenitybdd.annotations.*` | | `net.thucydides.core.util.EnvironmentVariables` | `net.thucydides.model.util.EnvironmentVariables` | | `net.serenitybdd.core.environment.EnvironmentSpecificConfiguration` | `net.serenitybdd.model.environment.EnvironmentSpecificConfiguration` | | `serenity-cucumber4/5/6`, `cucumber.api.*` | `serenity-cucumber`, `io.cucumber.*` | | `InputField.called(..)`, `PageElement.called(..)` | `InputField.withNameOrId(..)`, `PageElement.withNameOrId(..)` | | `drivers { os { webdriver.chrome.driver = ... } }` with bundled binaries | nothing: leave driver selection to Serenity and Selenium | | `serenity-jbehave` stories | no 5.x release: migrate to Cucumber | | `@RunWith(SerenityRunner.class)` | still valid with `serenity-junit` and JUnit 4; for JUnit 5 use `@ExtendWith(SerenityJUnit5Extension.class)` | | `net.thucydides.core.steps.ScenarioSteps`, `net.thucydides.core.pages.Pages` | **not old**: still in those packages | ## 7. Not covered Gradle plugin version history, Serenity 1.x, Appium and mobile modules, the Spring, JIRA and cloud-provider modules, and Selenium 3 to 4 API changes outside Serenity's own classes. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). --- # Reading Test Results Without the HTML Report Verified against Serenity **5.4.1** by running a passing and a failing project: the files, the fields and the script were all checked. An AI assistant, a script or a CI step can read the outcome of a run from files, not from the HTML report or the Maven console. Serenity writes them to `target/site/serenity/`. ## What is written, and when | File | Written | Contains | |---|---|---| | `<64 hex characters>.json`, one per test | **While the tests run.** It exists after a failed build, when no HTML report was generated | The full outcome of one test: title, result, requirement path, the step tree, and the failure | | `SERENITY-JUNIT-.xml`, one per test | While the tests run | The same test in standard JUnit XML, for CI tools | | `index.html`, the other `.html` pages, `summary.txt` | **Only if the report was generated.** `mvn verify` stops at a failed test, so after a failure they are missing (see the troubleshooting page) | The HTML report; `summary.txt` has the totals as prose | | `serenity-summary.json` | Only if the optional JSON summary report is enabled (below) | Totals and one entry per scenario | The JSON file name is a 64-character hash. It was the same on every run of the same test, but it tells you nothing about the test, so read the `title`, `testCaseName` and `methodName` fields inside. So after any run, including a failed one, **read `target/site/serenity/*.json`**. Do not wait for `index.html` and do not scrape it. ## The quickest way: `scripts/results.py` The script is in the skill (`scripts/results.py`) and in the serenity-core repository at `docs/ai/skill/serenity-bdd/scripts/results.py`; copy it into the project if the skill is not installed. ```bash python3 /scripts/results.py target/site/serenity # totals, a line per test, details of each failure python3 /scripts/results.py target/site/serenity --json # the same as one JSON document ``` It prints the totals, one line per test (result, title, requirement path, outcome file), and for each failed test the failing step, the exception type and message, the first stack frame in the project's own code, and the outcome file to open for the full detail. The exit code is 1 if any test failed, errored or was compromised, and 2 if there are no outcome files. A run of more than 40 tests lists only the tests that did not pass unless you add `--all`. It reads only the JSON files, uses no dependencies, and is a different tool from `outline.py` (which judges the wording and structure of the report, not the result of the run). When the failure is an assertion or a wait (for example Screenplay `WaitUntil` or `should(seeThat(..))`), Serenity may not mark any step as failed. The script then says so and gives the last step recorded; the stack frame names the line of your test that failed. ## The fields of an outcome file ```text title, name the scenario title as the report shows it result SUCCESS, FAILURE, ERROR, COMPROMISED, PENDING, SKIPPED, IGNORED ... testCaseName class name; methodName: the test method (JUnit) userStory { path, storyName, ... } the requirement it belongs to (package path or feature file) featureTag, tags the feature and any @tags startTime, duration duration in milliseconds testSource JUnit5, Cucumber ... testSteps the step tree: [ { number, description, result, duration, children: [ ... ] } ] ``` For a test that did not pass, the file also has: ```text testFailureCause { errorType, message, stackTrace: [ { declaringClass, methodName, fileName, lineNumber } ] } testFailureClassname the exception class testFailureMessage the exception message testFailureSummary "FAILURE;;;" ``` and the step that failed (when one is marked) has `exception` with the same `errorType`, `message` and `stackTrace`. REST steps carry `restQuery` with `method`, `path`, `requestHeaders`, `content`, `statusCode`, `responseHeaders` and `responseBody`, which is often all that is needed to see why an API test failed. Nested steps are in `children`. The step tree is long for big tests (every request, with its response body, is in it). Read the summary first and open one outcome file only for a test you are investigating. ## Optional: a JSON summary of the whole run The `serenity-json-summary-report` artifact writes one `serenity-summary.json` for the run. It is not enabled by default. Add it as a dependency of the Serenity Maven plugin and ask for the report: ```xml net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} net.serenity-bdd serenity-json-summary-report ${serenity.version} ``` ```bash mvn serenity:reports -Dserenity.reports=json-summary ``` `serenity-summary.json` has `report`, `results` (`counts`: total, success, failure, error, pending, skipped ...; durations), `resultsByFeature` (each feature with its scenarios: `title`, `result` and for each outcome an `errorMessage`), `frequentFailures`, `unstableFeatures`, `coverage` and `tags`. It has the failure message of each scenario but not the failing step, the stack trace or the link to the outcome file, so for investigating a failure use the outcome files (or `results.py`). The goal ran after a failed build and produced it from the outcome files, with no HTML report. ## What this is not - The outcome file format is Serenity's own. It is stable in practice but is not a published contract, so read the fields listed above and ignore the rest. - The JUnit XML (`SERENITY-JUNIT-*.xml`) and Maven's `target/surefire-reports/*.xml` are the standard formats; use them when a tool expects JUnit XML. ## What an assistant should do after a run 1. Run the build (`mvn verify`). If it fails, do not stop at the console output. 2. Run `results.py` on `target/site/serenity`. Fix compile errors from the console first; they produce no outcome files. 3. For each failed test, read the message and the stack frame in your own code, and decide whether the test or the application is wrong. If the application is wrong, say so rather than editing the test to pass. 4. When all tests pass, build the report (`mvn verify` does it when the tests pass) and review it with `outline.py` and the review checklist. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). --- # Reviewing Tests and Living Documentation Used by the `serenity-test-reviewer` agent after tests are written, and by anyone reviewing Serenity tests. Style-neutral: it applies to Page Objects, Screenplay and Cucumber, Selenium, Playwright and REST. Do not ask for a different style; judge the tests inside the style they use. Read `living-documentation.md` first for what Serenity does with titles, steps and structure. ## What to look at 1. The test sources (and the feature files). 2. The report as a reader sees it: run the tests, then `python3 /scripts/outline.py /target/site/serenity`. If the tests cannot run here, review the sources and say the report was not seen. 3. The task the user asked for, to check that the tests cover it. ## The checks Each check is PASS, WEAK or FAIL. Quote the line or title, and write the replacement. ### A. Intent: does a reader learn the business rule? - **A1** The scenario title states the rule or outcome in business words (not `testCheckout1`, not `shouldReturn201`, not a description of clicks). - **A2** The scenario is about one behaviour. A title with "and" covering two rules should be two scenarios. - **A3** It is clear why the behaviour matters or what the user gets (a title like "A reader sees an order number after paying" says it; "Checkout works" does not). - **A4** Cucumber: the feature has a short business description; scenario outlines use meaningful example column names. ### B. Steps: business language - **B1** Steps read as a narrated story with who/what, in the project's domain terms. - **B2** No selectors, ids, URLs, sleeps, driver calls or HTTP codes in step text, titles or the test body (they belong inside page objects, tasks, step libraries, step definitions). In a REST test, a status code is fine inside the check, not as the step name. - **B3** The final step/check states the business expectation ("the confirmation shows an order number"), and the check really proves that (see D1). - **B4** One level of abstraction per scenario: business tasks at the top, mechanics inside them. ### C. Organisation: easy to navigate - **C1** Scenarios are grouped by function (package/folder per capability, one class or feature file per feature, or `@Epic/@Feature/@Story`), not one big class and not by technical layer. - **C2** The report tree (from `outline.py`) reads like a table of contents of what the application does. Names are business phrases, with no class name showing ("When signing in test": the class needs `@DisplayName`) and no duplicates. - **C3** One grouping method for the whole project (not package structure mixed with `@Feature` annotations). - **C4** Description files exist where an area needs explaining (Cucumber `readme.md`, or `src/test/resources/stories//readme.md`) and are short and about the business. ### D. Test quality - **D1** The assertions check what the title claims, with specific expected values from the task (not "page loaded", not `isNotNull`, not the same value read back from the test's own input). Ask: if the application broke in the way the title describes, would this test fail? - **D2** No fixed sleeps. Waits are on a condition (Serenity waits, `WaitUntil`, Playwright auto-wait). - **D3** Scenarios are independent: no shared mutable state between tests, no dependence on order, no static fields for scenario state. - **D4** Test data is visible where it matters (the value a rule depends on is in the scenario, not hidden in a helper), and secrets are not hard-coded beyond what the task gave. - **D5** No duplicated flows: repeated step sequences are a task, step method or Background. - **D6** It follows the project's existing style, driver and runner, and does not mix styles in one test. The project builds and the tests were run or the reason is stated. - **D7** Covers what was asked, including the negative case when the task mentioned one, and nothing invented beyond the application's described behaviour. ## Output ``` VERDICT: READY | NEEDS CHANGES LIVING DOCUMENTATION AS A READER SEES IT FINDINGS (worst first) 1. [FAIL B2] src/test/java/.../CheckoutTest.java:31: step "Click #place-order" shows a selector Change to: Task.where("{0} places the order", ...) ... WHAT IS GOOD ``` READY means no FAIL and at most two WEAK, and the report was seen. Be specific and brief: every finding says where it is and what to write instead. Do not edit files; the author applies the changes, and a second review follows if there were FAILs. ## Examples These files compile against Serenity 5.4.1 (Playwright examples: 5.3.11). --- # Your First Web Test In this tutorial you'll write your very first Serenity BDD test, using JUnit 5 and Selenium. We'll start out simple, then explore some of the features that make Serenity BDD rather special. So let's get started! ## Pre-requisites To run this tutorial, you will need a few things installed on your machine: * **Java**: Serenity BDD is a Java library, so you'll need a recent JDK installed. JDK 17 or higher should be fine. * **A Java IDE**: You'll also need a Java Development Environment such as IntelliJ or Eclipse (and a working knowledge of Java). * **Git**: We'll be using a starter project on Github, and the sample code for this project lives on Github too, so I'll be assuming you have a basic understanding of Git. ## Creating Your First Serenity BDD Project We will be using the **[Serenity BDD with JUnit](https://github.com/serenity-bdd/serenity-junit-screenplay-starter)** template project to get a simple project up and running quickly. :::note We'll be using a fairly conventional test automation style in this tutorial, using action classes and page objects. In the next tutorial, we'll see how to do all this using the Screenplay Pattern. ::: Go to the [project template page on Github](https://github.com/serenity-bdd/serenity-junit-pageobjects-starter) and click on [Use This Template](https://github.com/serenity-bdd/serenity-junit-pageobjects-starter/generate). ![](img/junit-template.png) ## Writing a simple test Let's start with a simple test. We will run a simple search for the keyword "Cucumber" on the DuckDuckGo search engine: ![](img/duckduckgo.png) First of all, we create a new package for our DuckDuckGo tests. The Serenity test reports mirror the directory structure we use to organize our tests, so it helps to make the structure and the package name meaningful. ![](img/initial-directory-structure.png) ### Basic test structure Now we create a new test class (let's call it "WhenSearchingByKeyword"), and an empty test case (we can call it "theKeywordShouldAppearInTheResultsSidebar"). ```java package starter.duckduckgo; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) class WhenSearchingByKeyword { @Test void theKeywordShouldAppearInTheResultsSidebar() { } } ``` :::caution There are a few things to note here: - The `@ExtendWith` annotation tells JUnit that this test uses Serenity - don't forget this annotation or your test won't be recognised as a Serenity test - The `@Test` annotation uses the `org.junit.jupiter.api.Test` class, from JUnit 5. Be careful not to get this mixed up with the JUnit 4 annotation of the same name (`org.junit.Test`), otherwise your test won't run. ::: ### Setting up WebDriver Next, let's add the code Serenity will need to work. First of all, since this will be a web test, we need to add a WebDriver field. Serenity manages the WebDriver lifecycle for us (it even downloads the WebDriver binaries for us), so all we need to do is to declare a `WebDriver` field, and annotate it with the `@Managed` annotation. In the code below, I've also added the `driver` attribute and the `options` attribute to ensure the browser opens in headless mode: ```java import net.serenitybdd.annotations.Managed; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.WebDriver; @ExtendWith(SerenityJUnit5Extension.class) class WhenSearchingByKeyword { @Managed(driver = "chrome", options = "headless") WebDriver driver; @Test void theKeywordShouldAppearInTheResultsSidebar() { } } ``` This driver will be used throughout the test, including in any page objects or action classes (more on these later) that are created as part of the test. ### Creating Action Classes We could simply start using this `driver` variable to interact with the browser. That would work fine. But keeping your test code well organised and well structured is essential to keeping maintenance costs down. And Serenity BDD gives us a number of ways to do this. One of the simplest ways to do this is called _Action Classes_. Action Classes are small, reusable classes with methods that encapsulate key user actions. For example, we could break this test down into three steps: - Navigate to the DuckDuckGo home page - Search by the keyword "Cucumber" - Check that the main topic heading on the result page is "Cucumber" Let's start with the first action: navigating to the DuckDuckGo home page. Create a new class called `NavigateActions` in the same package and add the following: ```java import net.serenitybdd.core.steps.UIInteractions; public class NavigateActions extends UIInteractions { public void toTheDuckDuckGoSearchPage() { openUrl("https://duckduckgo.com/"); } } ``` A UI Interactions class is a class containing methods that can interact with a web page. When we extend the `UIInteractions` class, Serenity will automatically inject the `@Managed` WebDriver instance we saw earlier whenever we use an instance of this class in our tests. The `UIInteractions` class gives you access to a range of methods that you can use to interact with the page. For example, as we can see in this example, the `openUrl()` method is used to open a page at a specified URL. Next, let's move on to the second action, where we need to search for a specific term. Create a new class called `SearchActions`, and make it extend the `UIInteractions` class. ```java import net.serenitybdd.core.steps.UIInteractions; public class SearchActions extends UIInteractions {} ``` Now add a method to search by a given keyword, called `byKeyword`, which will do the actual search. Inside this method, we can use the `$` notation to search for elements on the page using CSS or XPath locators, like this: ```java import net.serenitybdd.core.steps.UIInteractions; import org.openqa.selenium.Keys; public class SearchActions extends UIInteractions { public void byKeyword(String keyword) { $("#searchbox_input").sendKeys(keyword, Keys.ENTER); } } ``` ### Creating a Page Component class The third step of our test is to read the title of the search results sidebar and check that it is the value we expect. ![](img/result-module-title.png) We can do this with a Page Component object. Page Components (which extend the `PageComponent` class) are similar to action classes, but focus on reading information from a particular part of a web page, rather than modeling user interactions. To read the title of the sidebar on this screen, create a new class called `SearchResultSidebar` with the following code: ```java import net.serenitybdd.core.pages.PageComponent; public class SearchResultSidebar extends PageComponent { public String heading() { return $("[data-testid=about] h2").getText(); } } ``` Once again we are locating the element using the `$` operator and a CSS selector, and returning the text value of the element we find. ### Using Action Classes and Page Components In The Test Now that we've created action classes for the first two steps, let's add them to our test. Serenity recognises action classes and page component objects and instantiates them automatically with the @Managed WebDriver instance, so we simply need to declare them as class fields, and then use them in our test. ```java import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import net.serenitybdd.annotations.Managed; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.WebDriver; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class WhenSearchingByKeyword { @Managed(driver = "chrome", options = "headless") WebDriver driver; NavigateActions navigate; SearchActions search; SearchResultSidebar searchResultSidebar; @Test void theKeywordShouldAppearInTheResultsSidebar() { navigate.toTheDuckDuckGoSearchPage(); search.byKeyword("Cucumber"); assertThat(searchResultSidebar.heading()).isEqualTo("Cucumber"); } } ``` ### Reporting the steps We now have a working test. You can run it from within your IDE, or by running `mvn verify` on the command line. However, there is one more step we can do. Serenity BDD is a living documentation tool, which means it aims to document not only what tests are executed, but how they are executed. We can control how Serenity reports each method in a `UIInteractions` class using the `@Step` annotation. When you place a `@Step` annotation on a `UIInteractions` method, this method will appear in the Serenity reports whenever it is called. Let's see how it works. Update the `NaviagateActions` and `SearchActions` classes to include the `@Step` annotations like this: ```java import net.serenitybdd.annotations.Step; import net.serenitybdd.core.steps.UIInteractions; public class NavigateActions extends UIInteractions { @Step("Navigate to the home page") public void toTheDuckDuckGoSearchPage() { openUrl("https://duckduckgo.com/"); } } ``` ```java import net.serenitybdd.annotations.Step; import net.serenitybdd.core.steps.UIInteractions; import org.openqa.selenium.Keys; public class SearchActions extends UIInteractions { @Step("Search for '{0}'") public void byKeyword(String keyword) { $("#searchbox_input").sendKeys(keyword, Keys.ENTER); } } ``` ### Reporting on assertions The last step of our test is an assertion. We can report on the assertion by wrapping it in the `Serenity.reportThat()` method, using a Lambda expression for the actual assertion: ```java @Test void theKeywordShouldAppearInTheResultsSidebar() { navigate.toTheDuckDuckGoSearchPage(); search.byKeyword("Cucumber"); Serenity.reportThat("The keyword should appear in the sidebar heading", () -> assertThat(searchResultSidebar.heading()).isEqualTo("Cucumber") ); } ``` ## Generating the reports Let's run the tests and see what the reports look like. Delete the `wikipedia` package (we don't need the sample tests anymore), and run `mvn verify` from the command line. You should see an output along the following lines: ```bash $ mvn verify ... [INFO] SERENITY TESTS: SUCCESS [INFO] ----------------------------------------- [INFO] | Tests executed | 1 [INFO] | Tests passed | 1 [INFO] | Tests failed | 0 [INFO] | Tests with errors | 0 [INFO] | Tests compromised | 0 [INFO] | Tests aborted | 0 [INFO] | Tests pending | 0 [INFO] | Tests ignored/skipped | 0 [INFO] ------------------------ | -------------- [INFO] | Total Duration | 5s 626ms [INFO] | Fastest test took | 5s 626ms [INFO] | Slowest test took | 5s 626ms [INFO] ----------------------------------------- [INFO] [INFO] SERENITY REPORTS [INFO] - Full Report: file:///Users/john/Projects/Serenity/serenity-new-documentation/serenity-users-guide/sample-code/docs-starter-tutorial/target/site/serenity/index.html [INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (default) @ serenity-junit-starter --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 21.729 s [INFO] Finished at: 2021-11-30T11:27:29Z [INFO] ------------------------------------------------------------------------ ``` Now open the `index.html` file in the `target/site/serenity` directory. You should see something like this: ![](img/report-home.png) Click on the "Test Results" tab and open the "When Searching By Keyword" scenario. This should show you the step-by-step details of your test: ![](img/report-keyword-search.png) ## Conclusion Congratulation! You've written your first Serenity BDD test, and seen a little of what Serenity BDD can do! --- # Your First Screenplay Scenario The Screenplay Pattern is a way to write clean, readable, scalable test automation modeled around the user. Screenplay builds on good software engineering principles such as the Single Responsibility Principle, the Open-Closed Principle, and effective use of Layers of Abstraction. It encourages good testing habits and well-designed test suites that are easy to read, easy to maintain and easy to extend, enabling teams to write more robust and more reliable automated tests more effectively. In this section, we will show you how to write your first Screenplay scenario using Serenity BDD. You will learn how to write a simple scenario for the well-known [TodoMVC](http://todomvc.com) project. ![](img/journey-todo-app.png) ## Pre-requisites To run this tutorial, you will need a few things installed on your machine: * **Java**: Serenity BDD is a Java library, so you'll need a recent JDK installed. JDK 17 or higher should be fine. * **A Java IDE**: You'll also need a Java Development Environment such as IntelliJ or Eclipse (and a working knowledge of Java). * **Git**: We'll be using a starter project on Github, and the sample code for this project lives on Github too, so I'll be assuming you have a basic understanding of Git. You will also need a Git client installed on your computer. ## Introducing the Screenplay Pattern The goal of the Screenplay pattern is to help you write cleaner, more readable, more maintainable test automation code for any domain. With Screenplay, your tests use an intuitive, readable style like the following: ```java wendy.attemptsTo( Navigate.toTheHomePage(), Search.byKeyword("Everest"), Ensure.that(DisplayedArticle.firstHeading()).isEqualTo("Mount Everest") ); ``` As you can see here, Screenplay uses a user-centric model, where we describe _actors_ who interact with an application in various ways to perform _tasks_ that help them achieve their goals. Actors have _abilities_, such as the ability to interact with a web browser or query a database, that help them perform these tasks. Actors can also answer _questions_ about the state of the system, so that we can check whether a feature is behaving the way we would expect it to. ![](img/screenplay-model.svg) ## Creating a Screenplay project ### Using the Serenity template projects The quickest way to start a new Screenplay project is to clone one of the starter projects. For this tutorial, we will start with the [Serenity JUnit Screenplay Starter project](https://github.com/serenity-bdd/serenity-junit-screenplay-starter), which uses Serenity Screenplay and JUnit 5. :::tip This starter project has a sample Screenplay test in the `src/test/java/starter/wikipedia` folder. You won't need this code for this tutorial so you can safely delete it. ::: ### The project directory structure We will use some simple conventions to organise our test classes, based on the standard Maven project structure you can see here: ![](img/screenplay-directory-layout.png) Our test code will be stored in two folders, underneath `src/test/java/todomvc`. The first directory named `features` will contain our test classes. The second, `screenplay`, will contain our Screenplay classes. Screenplay classes are designed to be highly modular and reusable, and often appear in many tests, so it makes sense to keep them separate from the tests themselves. Serenity organises JUnit test results based on the package structure we use, so we need to tell it the name of the root package where our tests will live. We do this in the `serenity.conf` file in the `src/test/resources` folder. Open this file and add the following line: ``` serenity.test.root = todomvc.features ``` ## Writing your first scenario ### Creating the test case For our first scenario, we will simply add a 'to do' item (say, "Buy some milk") to an empty list. Start by creating a new empty test case under the `src/test/java/todomvc/features` package like this: ```java package todomvc.features; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) class AddNewTodos { @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { } } ``` The line `@ExtendWith(SerenityJUnit5Extension.class)` indicates that this is a Serenity BDD test. ### Introducing the actor Screenplay uses an actor-centric approach, where our tests describe _actors_, who represent actual users of our application, and the actions they perform. In a Serenity BDD JUnit test, we can use the `@CastMember` annotation to declare an actor like this: ```java @CastMember(name = "Toby") Actor toby; ``` This will instantiate a new actor (called Toby) for our test, and assign a webdriver instance that he can use to interact with our application. ### Actors perform tasks and interactions In a Screenplay test, everything starts with the actor, who interacts with the system under test to achieve their goals. In this case, the first thing Toby needs to do is to open the TodoMvc application at a given URL. We do this by calling the `attemptsTo()` method on the `Actor` class. The `attemptsTo()` method takes a list of actions that the actor needs to perform. We call these actions _performables_. We generally talk about two types of _performables_: - _Interactions_, where the actor interacts directly with the application (Click a button or Open a URL), and - _Tasks_, which are groups of interactions that the actor needs to perform to achieve some goal (Add an item to the cart or purchase an item). An example of a simple Interaction is the `Open.url()` interaction, which opens a given URL. ```java Open.url("https://todomvc.com/examples/angularjs/#/") ``` In our "add a todo item to an empty list" test, the first step will be for Toby to open the browser on the TodoMVC application home page. We can do this with the following code: ```java @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { toby.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/") ); } ``` ### Actors can interact with a web application The next thing we need to do is to enter a value into the input field on the TodoMVC home page that you can see here: ![](img/todomvc-input.png) To enter a value into a field, we can use the `Enter` interaction class. This field can be located using a CSS locator like _".new-todo"_. So to enter the value into this field, we can use the following interaction: ```java Enter.theValue("Buy some milk").into(".new-todo") ``` There is no submit button on this form. To add the item to the list, we need to press the _RETURN_ key. We can do this by adding the `thenHit()` method, like this: ```java Enter.theValue("Buy some milk").into(".new-todo").thenHit(Keys.RETURN) ``` When we add this to our test, we get the following: ```java @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { toby.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/"), Enter.theValue("Buy some milk").into(".new-todo").thenHit(Keys.RETURN) ); } ``` ### Actors can ask about the state of the system Once Toby hits RETURN, the item will be added to the todo list, as we can see here: ![](img/todo-list-1-item.png) Now we need to check that the item we entered was successfully added to this list. In Screenplay we do this using _Questions_. A Question is a class that knows how to retrieve a specific piece of information about the state of the application. This could be the value of a text field, the result of a REST API call, or even the result of a database query. In this case, we can identify the names of the todo items in the list using a CSS locator such as ".todo-list li". So a _question_ to retrieve each of the todo items displayed on the page might look like this: ```java Text.ofEach(".todo-list li") ``` A question does nothing by itself - just as we need an actor to perform an interaction, we also need an actor to ask a question. One way we do this is to use the `asksFor()` method, like this: ```java var todos = toby.asksFor(Text.ofEach(".todo-list label")); ``` If we add an AssertJ assertion to check that the list contains the value we expect, our completed test will look like the following: ```java import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.actions.Enter; import net.serenitybdd.screenplay.actions.Open; import net.serenitybdd.screenplay.annotations.CastMember; import net.serenitybdd.screenplay.questions.Text; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.Keys; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class AddNewTodos { @CastMember(name = "Toby") Actor toby; @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { toby.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/"), Enter.theValue("Buy some milk").into(".new-todo").thenHit(Keys.RETURN) ); var todos = toby.asksFor(Text.ofEach(".todo-list li")); assertThat(todos).containsExactly("Buy some milk"); } } ``` ## Generating the reports Let's run the tests and see what the reports look like. Just run `mvn clean verify` from the command line. You should see an output along the following lines: ```bash $ mvn verify ... [INFO] ----------------------------------------- [INFO] SERENITY TESTS: SUCCESS [INFO] ----------------------------------------- [INFO] | Test cases executed | 1 [INFO] | Tests executed | 1 [INFO] | Tests passed | 1 [INFO] | Tests failed | 0 [INFO] | Tests with errors | 0 [INFO] | Tests compromised | 0 [INFO] | Tests aborted | 0 [INFO] | Tests pending | 0 [INFO] | Tests ignored/skipped | 0 [INFO] ------------------------ | -------------- [INFO] | Total Duration | 7s 292ms [INFO] | Fastest test took | 7s 292ms [INFO] | Slowest test took | 7s 292ms [INFO] ----------------------------------------- [INFO] [INFO] SERENITY REPORTS [INFO] - Full Report: file:///C:/Users/johns/projects/todomvc-screenplay-tutorial/target/site/serenity/index.html [INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (default) @ serenity-junit-screenplay-starter --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 21.660 s [INFO] Finished at: 2022-04-04T13:34:26+01:00 [INFO] ------------------------------------------------------------------------ ``` Now open the `index.html` file in the `target/site/serenity` directory using a web browser, and navigate to the Test Results tab. Open up the "Add a todo item to an empty list" scenario. Notice how the report renders the actor's actions almost word-for-word: ![](img/screenplay-report-1.png) ## Conclusion You have now seen just a little of the power of the Screenplay pattern. To learn more about what you can do with the Screenplay pattern, check out [the full Screenplay Documentation](../screenplay/screenplay_fundamentals). --- # Your First API Test In the previous tutorial we looked at how to write a simple web test with Serenity BDD. But Serenity isn't just for web tests: Serenity BDD also provides tight integration with [Rest Assured](https://rest-assured.io/), a popular open source library for testing REST APIs. ## Pre-requisites To run this tutorial, you will need a few things installed on your machine: * **Java**: Serenity BDD is a Java library, so you'll need a recent JDK installed. JDK 17 or higher should be fine. * **Maven**: You will need Maven 3 or higher installed in your computer. This acts as a build tool that also downloads the dependencies while building. * **A Java IDE**: You'll also need a Java Development Environment such as IntelliJ or Eclipse (and a working knowledge of Java). * **Git**: We'll be using a starter project on Github, and the sample code for this project lives on Github too, so I'll be assuming you have a basic understanding of Git. ## Creating your project We will be using the **[Serenity BDD Junit Starter](https://github.com/serenity-bdd/serenity-junit-starter)** template project to get a simple project up and running quickly. This project comes with a sample test based on Junit 5 already implemented for our reference. For now, we are going to ignore that and start writing a new test from scratch. Go to the [project template page on Github](https://github.com/serenity-bdd/serenity-junit-starter) and click on [Use This Template](https://github.com/serenity-bdd/serenity-junit-starter/generate). ## Deleting the unnecessary files Just to make sure that the starter template's sample files do not interfere with our experience in this tutorial, **delete** the `src/test/java/starter/wikipedia` directory. ## Adding the Serenity RestAssured Dependency Open the `pom.xml` file in the root directory and add the following lines in the `` section, similar to the ones already in the file. ```xml net.serenity-bdd serenity-rest-assured ${serenity.version} test ``` ## Enabling Detailed HTML reports When we use the template's default configuration, we get only a single-page HTML report. We want to generate a detailed HTML report in this tutorial. So, let us **delete** the following line from the `pom.xml` file. ```xml single-page-html ``` You can find this in the configuration section of `serenity-maven-plugin` plugin. ## The project directory structure We will use some simple conventions to organise our feature files and the supporting Java classes, based on the standard Maven project structure outlined below: ``` ├───src │ ├───main │ │ └───java │ │ └───starter │ └───test │ ├───java │ │ └───starter │ │ └───petstore │ └───resources ``` Here are some points to note about the directory structure. 1. Since we will test the publicly available [Pet Store API](https://petstore.swagger.io/), we will not have any code in the `src/main` directory. 2. We will re-use (from the template) the contents that are already there in the `src/test/resources/` directory. 3. We will create a new directory `petstore` under `src/test/java/starter` directory to store our test class and its helpers. ## Writing an API test Let us start with writing our API test. In this test, we will test the [`GET /pet/{petId}`](https://petstore.swagger.io/#/pet/getPetById) API. This API will return a pet when you give its `id` in the URL. However, we cannot call this API directly without any `id`. Hence, our test needs to first create a Pet and get its `id` before it calling the `GET /pet/{petId}` API end point. In other words, we could write our test in the `Given-When-Then` format as follows. ```Gherkin Given Kitty is available in the pet store When I ask for a pet using Kitty's ID Then I get Kitty as result ``` ### Basic test structure Now we create a new test class (let's call it `WhenFetchingAlreadyAvailablePet`), and an empty test case (we can call it `fetchAlreadyAvailablePet`). ```java package starter.petstore; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) public class WhenFetchingAlreadyAvailablePet { @Test public void fetchAlreadyAvailablePet() { } } ``` :::caution There are a few things to note here: - The `@ExtendWith` annotation tells JUnit that this test uses Serenity - don't forget this annotation or your test won't be recognised as a Serenity test - The `@Test` annotation uses the `org.junit.jupiter.api.Test` class, from JUnit 5. Be careful not to get this mixed up with the JUnit 4 annotation of the same name (`org.junit.Test`), otherwise your test won't run. - Note that the name of the test class starts with `When`. This is one of the ways to make sure it is recognized as a test to be run in the Maven build process. See the `pom.xml` file's `configuration` section for `maven-failsafe-plugin` for more details on this. ::: ### Creating Action Classes We could simply start writing the entire test's code inside our `fetchAlreadyAvailablePet()` method. That would work fine. But keeping your test code well organised and well-structured is essential to keeping maintenance costs down. And Serenity BDD gives us a number of ways to do this. One of the simplest ways to do this is called _Action Classes_. Action Classes are small, reusable classes with methods that encapsulate key user actions. We will use these classes to call the HTTP APIs. For example, we could break our `fetchAlreadyAvailablePet()` test down into three steps: 1. **Arrange (Given):** Set the stage to call the GET api by pre-loading the pet named 'Kitty' using a HTTP POST call documented [here](https://petstore.swagger.io/#/pet/addPet). 2. **Act (When):** Call the API under test using the ID of 'Kitty' 3. **Assert (Then):** Check that the API returns a pet with name 'Kitty' Let us create an action class named `PetApiActions` with the following skeleton code, in the same `petstore` package where the test lives. ```java package starter.petstore; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import net.serenitybdd.core.steps.UIInteractions; public class PetApiActions extends UIInteractions { @Given("Kitty is available in the pet store") public Long givenKittyIsAvailableInPetStore() { } @When("I ask for a pet using Kitty's ID: {0}") public void whenIAskForAPetWithId(Long id) { } @Then("I get Kitty as result") public void thenISeeKittyAsResult() { } } ``` :::caution **Points to note** 1. Since we want to use the ID generated by the API in our upcoming step, we return the ID as a Long return value. 2. We extend the `UIInteractions` class that comes with the Serenity BDD to help us interact with APIs. ::: Let's start with implementing the first action: Setting the stage by pre-creating a pet with name `"Kitty"` by calling the POST API. Since we need to create a Java object to hold the `Pet`, we can create a class `Pet.java` under `starter.petstore` package with the following code. ```java package starter.petstore; public class Pet { private String name; private String status; private Long id; public Pet(String name, String status, Long id) { this.name = name; this.status = status; this.id = id; } public Pet(String name, String status) { this.name = name; this.status = status; } public String getName() { return this.name; } public String getStatus() { return this.status; } public Long getId() { return id; } public void setName(String name) { this.name = name; } public void setStatus(String status) { this.status = status; } public void setId(Long id) { this.id = id; } } ``` Now that we have a way to represent a Pet in our code, let us write our first action in function `givenKittyIsAvailableInPetStore()` of the class `PetApiActions`. ```java package starter.petstore; 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.mapper.ObjectMapperType; import static net.serenitybdd.rest.SerenityRest.given; import net.serenitybdd.core.steps.UIInteractions; import static net.serenitybdd.rest.SerenityRest.*; public class PetApiActions extends UIInteractions { @Given("Kitty is available in the pet store") public Long givenKittyIsAvailableInPetStore() { Pet pet = new Pet("Kitty", "available"); Long newId = given() .baseUri("https://petstore.swagger.io") .basePath("/v2/pet") .body(pet, ObjectMapperType.GSON) .accept(ContentType.JSON) .contentType(ContentType.JSON).post().getBody().as(Pet.class, ObjectMapperType.GSON).getId(); return newId; } @When("I ask for a pet using Kitty's ID: {0}") public void whenIAskForAPetWithId(Long id) { } @Then("I get Kitty as result") public void thenISeeKittyAsResult() { } } ``` Next, let us write the implementation for the `whenIAskForAPetWithId` function. This will include just calling the GET API that needs to be tested. ```java @When("I ask for a pet using Kitty's ID: {0}") public void whenIAskForAPetWithId(Long id) { when().get("/" + id); } ``` :::caution **Points to note** 1. In the `get` method call above, the `baseUri` and `basePath` from the `given()` section is reused. This is why you did not have to repeat those details in this method. 2. Since we are using the `id` as an input parameter, we are using `{0}` in the description so that it can appear in our reports. ::: Next, let us write the implementation for the `thenISeeKittyAsResult` method as follows. ```java @Then("I get Kitty as result") public void thenISeeKittyAsResult() { then().body("name", Matchers.equalTo("Kitty")); } ``` Putting everything together, the `PetApiActions.java` file looks as follows. ```java package starter.petstore; 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.mapper.ObjectMapperType; import static net.serenitybdd.rest.SerenityRest.then; import static net.serenitybdd.rest.SerenityRest.when; import static net.serenitybdd.rest.SerenityRest.given; import net.serenitybdd.core.steps.UIInteractions; import org.hamcrest.Matchers; import static net.serenitybdd.rest.SerenityRest.*; public class PetApiActions extends UIInteractions { @Given("Kitty is available in the pet store") public Long givenKittyIsAvailableInPetStore() { Pet pet = new Pet("Kitty", "available"); Long newId = given() .baseUri("https://petstore.swagger.io") .basePath("/v2/pet") .body(pet, ObjectMapperType.GSON) .accept(ContentType.JSON) .contentType(ContentType.JSON).post().getBody().as(Pet.class, ObjectMapperType.GSON).getId(); return newId; } @When("I ask for a pet using Kitty's ID: {0}") public void whenIAskForAPetWithId(Long id) { when().get("/" + id); } @Then("I get Kitty as result") public void thenISeeKittyAsResult() { then().body("name", Matchers.equalTo("Kitty")); } } ``` ### Completing our test case Now that we have our Actions class ready, let us finish writing our test case in the `WhenFetchingAlreadyAvailablePet` class. ```java package starter.petstore; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) public class WhenFetchingAlreadyAvailablePet { Long newPetId = null; PetApiActions petApi; @Test public void fetchAlreadyAvailablePet() { newPetId = petApi.givenKittyIsAvailableInPetStore(); petApi.whenIAskForAPetWithId(newPetId); petApi.thenISeeKittyAsResult(); } } ``` Let us try running the Maven build to see the result. If the API works as expected, we expect the test to pass and a detailed HTML report to be generated. Run the following command in a terminal or command prompt. ```bash mvn clean verify ``` Once the command completes, you can see output similar to the following. ``` [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 10.009 s - in starter.petstore.WhenFetchingAlreadyAvailablePet [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 ... ... ... [INFO] ----------------------------------------- [INFO] SERENITY TESTS: SUCCESS [INFO] ----------------------------------------- [INFO] | Test cases executed | 1 [INFO] | Tests executed | 1 [INFO] | Tests passed | 1 [INFO] | Tests failed | 0 [INFO] | Tests with errors | 0 [INFO] | Tests compromised | 0 [INFO] | Tests aborted | 0 [INFO] | Tests pending | 0 [INFO] | Tests ignored/skipped | 0 [INFO] ------------------------ | -------------- [INFO] | Total Duration | 9s 212ms [INFO] | Fastest test took | 9s 212ms [INFO] | Slowest test took | 9s 212ms [INFO] ----------------------------------------- [INFO] [INFO] SERENITY REPORTS [INFO] - Full Report: file:///C:/Users/calib/source-codes/temp/serenity-junit-starter/target/site/serenity/index.html [INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (default) @ serenity-junit-starter --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 39.104 s [INFO] Finished at: 2022-09-02T17:33:14+05:30 [INFO] ------------------------------------------------------------------------ ``` Yes, the test passed and the build is successful now. We have managed to test our API successfully 🎉 ## Reporting and Living Documentation If you notice carefully, the output of the `mvn clean verify` command informed us that a report is created in `target/site/serenity/index.html` When you open this file in a web browser, you see a beautiful report like this. ![Report Home Page](img/rest-report-home.png) You can also find the test-wise results detailing the steps in the `Stories` tab, detailing the REST API calls. ![Report](img/rest-report-stories.jpg) If you want to look at the exact details used in the HTTP requests, you can click on the link circled in the above screenshot. This will show you the details as shown below. ![Report with HTTP requests](img/rest-report-http-requests.png) Feel free to navigate the links in this report and look around. ## Conclusion In this tutorial, you created your own API test cases and ran them using Serenity BDD to generate a beautiful report. --- # Comprehensive REST API Testing with Serenity BDD This tutorial provides a complete guide to testing REST APIs with Serenity BDD, covering everything from basic requests to advanced patterns like authentication, error handling, schema validation, and hybrid UI+API testing. All examples use real, publicly available testing APIs that you can use to practice. ## Testing APIs Used | API | URL | Purpose | |-----|-----|---------| | JSONPlaceholder | https://jsonplaceholder.typicode.com | Free fake API for basic CRUD | | ReqRes | https://reqres.in | User management with auth examples | | Restful-Booker | https://restful-booker.herokuapp.com | Hotel bookings with token auth | ## Project Setup ### Maven Dependencies ```xml 5.4.1 net.serenity-bdd serenity-core ${serenity.version} test net.serenity-bdd serenity-rest-assured ${serenity.version} test net.serenity-bdd serenity-screenplay-rest ${serenity.version} test net.serenity-bdd serenity-junit5 ${serenity.version} test io.rest-assured json-schema-validator 5.3.2 test ``` ### Gradle Dependencies ```groovy dependencies { testImplementation "net.serenity-bdd:serenity-core:5.4.1" testImplementation "net.serenity-bdd:serenity-rest-assured:5.4.1" testImplementation "net.serenity-bdd:serenity-screenplay-rest:5.4.1" testImplementation "net.serenity-bdd:serenity-junit5:5.4.1" testImplementation "io.rest-assured:json-schema-validator:5.3.2" } ``` ### Configuration (serenity.conf) ```hocon # Base URLs for different environments restapi { jsonplaceholder = "https://jsonplaceholder.typicode.com" reqres = "https://reqres.in/api" booker = "https://restful-booker.herokuapp.com" } # Request logging serenity { logging = VERBOSE } # REST Assured settings restassured { # Log all requests and responses log.all = true } ``` --- ## Part 1: Basic REST Operations ### Simple GET Request Let's start with the simplest possible API test - fetching a resource: ```java package com.example.api; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.rest.SerenityRest; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.rest.SerenityRest.given; import static org.hamcrest.Matchers.*; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Basic REST Operations") class BasicRestTest { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; @Test @DisplayName("Should fetch a single user") void shouldFetchSingleUser() { given() .baseUri(BASE_URL) .when() .get("/users/1") .then() .statusCode(200) .body("id", equalTo(1)) .body("name", equalTo("Leanne Graham")) .body("email", equalTo("Sincere@april.biz")); } @Test @DisplayName("Should fetch all users") void shouldFetchAllUsers() { given() .baseUri(BASE_URL) .when() .get("/users") .then() .statusCode(200) .body("size()", equalTo(10)) .body("name", hasItems("Leanne Graham", "Ervin Howell")); } } ``` ### POST Request - Creating Resources ```java @Test @DisplayName("Should create a new post") void shouldCreateNewPost() { String requestBody = """ { "title": "My New Post", "body": "This is the content of my post", "userId": 1 } """; given() .baseUri(BASE_URL) .contentType("application/json") .body(requestBody) .when() .post("/posts") .then() .statusCode(201) .body("title", equalTo("My New Post")) .body("id", notNullValue()); } ``` ### Using POJOs for Request/Response Create model classes for cleaner code: ```java // Post.java public class Post { private Integer id; private String title; private String body; private Integer userId; // Default constructor for JSON deserialization public Post() {} public Post(String title, String body, Integer userId) { this.title = title; this.body = body; this.userId = userId; } // Getters and setters public Integer getId() { return id; } public void setId(Integer id) { this.id = id; } public String getTitle() { return title; } public void setTitle(String title) { this.title = title; } public String getBody() { return body; } public void setBody(String body) { this.body = body; } public Integer getUserId() { return userId; } public void setUserId(Integer userId) { this.userId = userId; } } ``` ```java @Test @DisplayName("Should create post using POJO") void shouldCreatePostUsingPojo() { Post newPost = new Post("POJO Title", "Content from POJO", 1); Post createdPost = given() .baseUri(BASE_URL) .contentType("application/json") .body(newPost) .when() .post("/posts") .then() .statusCode(201) .extract() .as(Post.class); assertThat(createdPost.getTitle()).isEqualTo("POJO Title"); assertThat(createdPost.getId()).isNotNull(); } ``` ### PUT and PATCH Requests ```java @Test @DisplayName("Should update entire post with PUT") void shouldUpdatePostWithPut() { Post updatedPost = new Post("Updated Title", "Updated Body", 1); given() .baseUri(BASE_URL) .contentType("application/json") .body(updatedPost) .when() .put("/posts/1") .then() .statusCode(200) .body("title", equalTo("Updated Title")); } @Test @DisplayName("Should partially update post with PATCH") void shouldPartiallyUpdateWithPatch() { given() .baseUri(BASE_URL) .contentType("application/json") .body("{\"title\": \"Patched Title\"}") .when() .patch("/posts/1") .then() .statusCode(200) .body("title", equalTo("Patched Title")) .body("body", notNullValue()); // Original body preserved } ``` ### DELETE Request ```java @Test @DisplayName("Should delete a post") void shouldDeletePost() { given() .baseUri(BASE_URL) .when() .delete("/posts/1") .then() .statusCode(200); } ``` --- ## Part 2: Screenplay Pattern for API Testing The Screenplay pattern provides a more maintainable, actor-centric approach: ### Setting Up Actors with API Abilities ```java package com.example.api.screenplay; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.rest.abilities.CallAnApi; import net.serenitybdd.screenplay.rest.interactions.*; import org.junit.jupiter.api.*; import org.junit.jupiter.api.extension.ExtendWith; import static net.serenitybdd.screenplay.rest.questions.ResponseConsequence.seeThatResponse; import static org.hamcrest.Matchers.*; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Screenplay REST API Tests") class ScreenplayRestTest { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; Actor alex; @BeforeEach void setUp() { alex = Actor.named("Alex the API tester") .whoCan(CallAnApi.at(BASE_URL)); } @Test @DisplayName("Should fetch user with Screenplay") void shouldFetchUserWithScreenplay() { alex.attemptsTo( Get.resource("/users/1") ); alex.should( seeThatResponse("User details are correct", response -> response .statusCode(200) .body("name", equalTo("Leanne Graham")) ) ); } @Test @DisplayName("Should create post with Screenplay") void shouldCreatePostWithScreenplay() { alex.attemptsTo( Post.to("/posts") .with(request -> request .contentType("application/json") .body(new Post("Screenplay Post", "Content", 1)) ) ); alex.should( seeThatResponse("Post was created", response -> response .statusCode(201) .body("title", equalTo("Screenplay Post")) ) ); } } ``` ### Creating Reusable Tasks Encapsulate API operations in reusable tasks: ```java // tasks/CreatePost.java package com.example.api.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.rest.interactions.Post; import net.serenitybdd.annotations.Step; import com.example.api.model.Post; import static net.serenitybdd.screenplay.Tasks.instrumented; public class CreatePost implements Task { private final Post post; public CreatePost(Post post) { this.post = post; } public static CreatePost withDetails(String title, String body, int userId) { return instrumented(CreatePost.class, new Post(title, body, userId)); } public static CreatePost withTitle(String title) { return instrumented(CreatePost.class, new Post(title, "Default body", 1)); } @Override @Step("{0} creates a new post titled '#post.title'") public void performAs(T actor) { actor.attemptsTo( Post.to("/posts") .with(request -> request .contentType("application/json") .body(post) ) ); } } ``` ```java // tasks/FetchUser.java package com.example.api.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.rest.interactions.Get; import net.serenitybdd.annotations.Step; import static net.serenitybdd.screenplay.Tasks.instrumented; public class FetchUser implements Task { private final int userId; public FetchUser(int userId) { this.userId = userId; } public static FetchUser withId(int userId) { return instrumented(FetchUser.class, userId); } @Override @Step("{0} fetches user with ID #userId") public void performAs(T actor) { actor.attemptsTo( Get.resource("/users/{id}") .with(request -> request.pathParam("id", userId)) ); } } ``` Using the tasks: ```java @Test @DisplayName("Should use custom tasks for API operations") void shouldUseCustomTasks() { alex.attemptsTo( CreatePost.withDetails("Task-based Post", "Content from task", 1) ); alex.should( seeThatResponse("Post was created successfully", response -> response.statusCode(201) ) ); alex.attemptsTo( FetchUser.withId(1) ); alex.should( seeThatResponse("User was retrieved", response -> response .statusCode(200) .body("name", notNullValue()) ) ); } ``` ### Creating Reusable Questions ```java // questions/TheUser.java package com.example.api.questions; import net.serenitybdd.rest.SerenityRest; import net.serenitybdd.screenplay.Question; import com.example.api.model.User; public class TheUser { public static Question details() { return actor -> SerenityRest.lastResponse() .jsonPath() .getObject("", User.class); } public static Question name() { return actor -> SerenityRest.lastResponse() .jsonPath() .getString("name"); } public static Question email() { return actor -> SerenityRest.lastResponse() .jsonPath() .getString("email"); } } ``` ```java @Test @DisplayName("Should use questions to query responses") void shouldUseQuestionsToQueryResponses() { alex.attemptsTo( FetchUser.withId(1) ); String userName = alex.asksFor(TheUser.name()); assertThat(userName).isEqualTo("Leanne Graham"); User user = alex.asksFor(TheUser.details()); assertThat(user.getEmail()).isEqualTo("Sincere@april.biz"); } ``` --- ## Part 3: Authentication Patterns ### Basic Authentication ```java @Test @DisplayName("Should authenticate with Basic Auth") void shouldAuthenticateWithBasicAuth() { given() .baseUri("https://httpbin.org") .auth().basic("user", "passwd") .when() .get("/basic-auth/user/passwd") .then() .statusCode(200) .body("authenticated", equalTo(true)); } ``` ### Bearer Token Authentication Using the Restful-Booker API for token-based auth: ```java @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Token Authentication Tests") class TokenAuthTest { private static final String BOOKER_URL = "https://restful-booker.herokuapp.com"; private String authToken; @BeforeEach void authenticate() { // Get authentication token authToken = given() .baseUri(BOOKER_URL) .contentType("application/json") .body(""" { "username": "admin", "password": "password123" } """) .when() .post("/auth") .then() .statusCode(200) .extract() .path("token"); assertThat(authToken).isNotNull(); } @Test @DisplayName("Should use token for authenticated requests") void shouldUseTokenForAuthenticatedRequests() { // Create a booking String bookingBody = """ { "firstname": "John", "lastname": "Doe", "totalprice": 150, "depositpaid": true, "bookingdates": { "checkin": "2024-01-01", "checkout": "2024-01-05" }, "additionalneeds": "Breakfast" } """; int bookingId = given() .baseUri(BOOKER_URL) .contentType("application/json") .body(bookingBody) .when() .post("/booking") .then() .statusCode(200) .extract() .path("bookingid"); // Update booking using token given() .baseUri(BOOKER_URL) .contentType("application/json") .cookie("token", authToken) // Token in cookie .body(""" { "firstname": "Jane", "lastname": "Doe", "totalprice": 200, "depositpaid": true, "bookingdates": { "checkin": "2024-01-01", "checkout": "2024-01-05" }, "additionalneeds": "Dinner" } """) .when() .put("/booking/" + bookingId) .then() .statusCode(200) .body("firstname", equalTo("Jane")); } @Test @DisplayName("Should use Authorization header for token") void shouldUseAuthorizationHeader() { given() .baseUri(BOOKER_URL) .header("Authorization", "Bearer " + authToken) .when() .get("/booking") .then() .statusCode(200); } } ``` ### API Key Authentication ```java @Test @DisplayName("Should authenticate with API key in header") void shouldAuthenticateWithApiKey() { given() .baseUri("https://api.example.com") .header("X-API-Key", "your-api-key-here") .when() .get("/protected-resource") .then() .statusCode(200); } @Test @DisplayName("Should authenticate with API key in query parameter") void shouldAuthenticateWithApiKeyInQuery() { given() .baseUri("https://api.example.com") .queryParam("api_key", "your-api-key-here") .when() .get("/protected-resource") .then() .statusCode(200); } ``` ### Screenplay Authentication Task ```java // tasks/Authenticate.java package com.example.api.tasks; import net.serenitybdd.rest.SerenityRest; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.rest.interactions.Post; import net.serenitybdd.annotations.Step; import static net.serenitybdd.screenplay.Tasks.instrumented; public class Authenticate implements Task { private final String username; private final String password; public Authenticate(String username, String password) { this.username = username; this.password = password; } public static Authenticate withCredentials(String username, String password) { return instrumented(Authenticate.class, username, password); } public static Authenticate asAdmin() { return instrumented(Authenticate.class, "admin", "password123"); } @Override @Step("{0} authenticates as #username") public void performAs(T actor) { actor.attemptsTo( Post.to("/auth") .with(request -> request .contentType("application/json") .body(String.format(""" {"username": "%s", "password": "%s"} """, username, password)) ) ); String token = SerenityRest.lastResponse().path("token"); actor.remember("authToken", token); } } ``` Using the authentication task: ```java @Test @DisplayName("Should authenticate and use token in subsequent requests") void shouldAuthenticateAndUseToken() { Actor admin = Actor.named("Admin") .whoCan(CallAnApi.at(BOOKER_URL)); admin.attemptsTo( Authenticate.asAdmin() ); String token = admin.recall("authToken"); assertThat(token).isNotNull(); // Use token in subsequent request admin.attemptsTo( Get.resource("/booking") .with(request -> request.cookie("token", token)) ); admin.should( seeThatResponse(response -> response.statusCode(200)) ); } ``` --- ## Part 4: Error Handling and Validation ### Testing Error Responses ```java @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Error Handling Tests") class ErrorHandlingTest { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; @Test @DisplayName("Should handle 404 Not Found") void shouldHandle404NotFound() { given() .baseUri(BASE_URL) .when() .get("/users/99999") .then() .statusCode(404); } @Test @DisplayName("Should handle invalid request body") void shouldHandleInvalidRequestBody() { given() .baseUri("https://reqres.in/api") .contentType("application/json") .body("{invalid json}") .when() .post("/users") .then() .statusCode(anyOf(equalTo(400), equalTo(500))); } @Test @DisplayName("Should verify error message content") void shouldVerifyErrorMessageContent() { given() .baseUri("https://reqres.in/api") .when() .post("/login") // Missing required fields .then() .statusCode(400) .body("error", equalTo("Missing email or username")); } } ``` ### Testing Multiple Status Codes ```java @ParameterizedTest @CsvSource({ "1, 200", "2, 200", "99999, 404" }) @DisplayName("Should return correct status for user ID") void shouldReturnCorrectStatusForUserId(int userId, int expectedStatus) { given() .baseUri("https://reqres.in/api") .when() .get("/users/" + userId) .then() .statusCode(expectedStatus); } ``` ### JSON Schema Validation Create a schema file at `src/test/resources/schemas/user-schema.json`: ```json { "$schema": "http://json-schema.org/draft-07/schema#", "type": "object", "required": ["data"], "properties": { "data": { "type": "object", "required": ["id", "email", "first_name", "last_name", "avatar"], "properties": { "id": { "type": "integer" }, "email": { "type": "string", "format": "email" }, "first_name": { "type": "string", "minLength": 1 }, "last_name": { "type": "string", "minLength": 1 }, "avatar": { "type": "string", "format": "uri" } } } } } ``` ```java import static io.restassured.module.jsv.JsonSchemaValidator.matchesJsonSchemaInClasspath; @Test @DisplayName("Should match JSON schema") void shouldMatchJsonSchema() { given() .baseUri("https://reqres.in/api") .when() .get("/users/1") .then() .statusCode(200) .body(matchesJsonSchemaInClasspath("schemas/user-schema.json")); } ``` ### Response Time Validation ```java import static java.util.concurrent.TimeUnit.SECONDS; import static java.util.concurrent.TimeUnit.MILLISECONDS; @Test @DisplayName("Should respond within acceptable time") void shouldRespondWithinAcceptableTime() { given() .baseUri(BASE_URL) .when() .get("/users") .then() .statusCode(200) .time(lessThan(5L), SECONDS); } @Test @DisplayName("Should track response time") void shouldTrackResponseTime() { long responseTime = given() .baseUri(BASE_URL) .when() .get("/users") .then() .extract() .time(); System.out.println("Response time: " + responseTime + "ms"); assertThat(responseTime).isLessThan(5000); } ``` --- ## Part 5: Chained API Calls Often you need to use data from one API call in subsequent calls: ```java @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Chained API Calls") class ChainedApiTest { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; @Test @DisplayName("Should create post and then fetch it") void shouldCreatePostAndThenFetchIt() { // Step 1: Create a new post Post newPost = new Post("Chained Test", "Testing chained calls", 1); Integer postId = given() .baseUri(BASE_URL) .contentType("application/json") .body(newPost) .when() .post("/posts") .then() .statusCode(201) .extract() .path("id"); // Step 2: Fetch the created post given() .baseUri(BASE_URL) .when() .get("/posts/" + postId) .then() .statusCode(200) .body("title", equalTo("Chained Test")); } @Test @DisplayName("Should fetch user and then their posts") void shouldFetchUserAndTheirPosts() { // Step 1: Get user int userId = given() .baseUri(BASE_URL) .when() .get("/users/1") .then() .statusCode(200) .extract() .path("id"); // Step 2: Get posts by that user given() .baseUri(BASE_URL) .queryParam("userId", userId) .when() .get("/posts") .then() .statusCode(200) .body("size()", greaterThan(0)) .body("userId", everyItem(equalTo(userId))); } @Test @DisplayName("Should perform CRUD lifecycle") void shouldPerformCrudLifecycle() { // CREATE Integer postId = given() .baseUri(BASE_URL) .contentType("application/json") .body(new Post("CRUD Test", "Initial content", 1)) .when() .post("/posts") .then() .statusCode(201) .extract() .path("id"); System.out.println("Created post with ID: " + postId); // READ given() .baseUri(BASE_URL) .when() .get("/posts/" + postId) .then() .statusCode(200) .body("title", equalTo("CRUD Test")); // UPDATE given() .baseUri(BASE_URL) .contentType("application/json") .body(new Post("Updated CRUD Test", "Updated content", 1)) .when() .put("/posts/" + postId) .then() .statusCode(200) .body("title", equalTo("Updated CRUD Test")); // DELETE given() .baseUri(BASE_URL) .when() .delete("/posts/" + postId) .then() .statusCode(200); } } ``` ### Screenplay Chained Calls ```java @Test @DisplayName("Should chain API calls with Screenplay") void shouldChainApiCallsWithScreenplay() { Actor tester = Actor.named("API Tester") .whoCan(CallAnApi.at(BASE_URL)); // Create post and remember the ID tester.attemptsTo( Post.to("/posts") .with(request -> request .contentType("application/json") .body(new Post("Screenplay Chain", "Content", 1)) ) ); Integer postId = SerenityRest.lastResponse().path("id"); tester.remember("postId", postId); // Fetch the post using remembered ID tester.attemptsTo( Get.resource("/posts/{id}") .with(request -> request.pathParam("id", tester.recall("postId"))) ); tester.should( seeThatResponse(response -> response .statusCode(200) .body("title", equalTo("Screenplay Chain")) ) ); } ``` --- ## Part 6: Data-Driven API Testing ### Parameterized Tests ```java @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Data-Driven API Tests") class DataDrivenApiTest { private static final String BASE_URL = "https://jsonplaceholder.typicode.com"; @ParameterizedTest @ValueSource(ints = {1, 2, 3, 4, 5}) @DisplayName("Should fetch user by ID") void shouldFetchUserById(int userId) { given() .baseUri(BASE_URL) .when() .get("/users/" + userId) .then() .statusCode(200) .body("id", equalTo(userId)); } @ParameterizedTest @CsvSource({ "1, Leanne Graham, Sincere@april.biz", "2, Ervin Howell, Shanna@melissa.tv", "3, Clementine Bauch, Nathan@yesenia.net" }) @DisplayName("Should verify user details") void shouldVerifyUserDetails(int id, String name, String email) { given() .baseUri(BASE_URL) .when() .get("/users/" + id) .then() .statusCode(200) .body("name", equalTo(name)) .body("email", equalTo(email)); } @ParameterizedTest @MethodSource("postDataProvider") @DisplayName("Should create posts with various data") void shouldCreatePostsWithVariousData(Post post) { given() .baseUri(BASE_URL) .contentType("application/json") .body(post) .when() .post("/posts") .then() .statusCode(201) .body("title", equalTo(post.getTitle())); } static Stream postDataProvider() { return Stream.of( new Post("First Post", "Content 1", 1), new Post("Second Post", "Content 2", 2), new Post("Third Post", "Content 3", 3) ); } } ``` ### Loading Test Data from Files Create a test data file `src/test/resources/testdata/users.json`: ```json [ {"userId": 1, "expectedName": "Leanne Graham"}, {"userId": 2, "expectedName": "Ervin Howell"}, {"userId": 3, "expectedName": "Clementine Bauch"} ] ``` ```java @ParameterizedTest @MethodSource("loadUsersFromFile") @DisplayName("Should verify users from JSON file") void shouldVerifyUsersFromFile(int userId, String expectedName) { given() .baseUri(BASE_URL) .when() .get("/users/" + userId) .then() .statusCode(200) .body("name", equalTo(expectedName)); } static Stream loadUsersFromFile() throws Exception { ObjectMapper mapper = new ObjectMapper(); List> users = mapper.readValue( new File("src/test/resources/testdata/users.json"), new TypeReference>>() {} ); return users.stream() .map(user -> Arguments.of( ((Number) user.get("userId")).intValue(), user.get("expectedName") )); } ``` --- ## Part 7: File Uploads and Downloads ### File Upload (Multipart) ```java @Test @DisplayName("Should upload a file") void shouldUploadFile() { File testFile = new File("src/test/resources/testdata/sample.txt"); given() .baseUri("https://httpbin.org") .multiPart("file", testFile) .when() .post("/post") .then() .statusCode(200) .body("files.file", notNullValue()); } @Test @DisplayName("Should upload file with additional form data") void shouldUploadFileWithFormData() { File testFile = new File("src/test/resources/testdata/sample.txt"); given() .baseUri("https://httpbin.org") .multiPart("file", testFile) .formParam("description", "Test file upload") .formParam("category", "documents") .when() .post("/post") .then() .statusCode(200); } ``` ### File Download ```java @Test @DisplayName("Should download a file") void shouldDownloadFile() { byte[] fileContent = given() .baseUri("https://httpbin.org") .when() .get("/image/png") .then() .statusCode(200) .contentType("image/png") .extract() .asByteArray(); assertThat(fileContent.length).isGreaterThan(0); // Optionally save to file // Files.write(Paths.get("downloaded.png"), fileContent); } ``` --- ## Part 8: Advanced Configuration ### Request Specification (Reusable Configuration) ```java @ExtendWith(SerenityJUnit5Extension.class) class RequestSpecificationTest { private RequestSpecification baseSpec; @BeforeEach void setup() { baseSpec = new RequestSpecBuilder() .setBaseUri("https://jsonplaceholder.typicode.com") .setContentType("application/json") .addHeader("Accept", "application/json") .log(LogDetail.ALL) .build(); } @Test @DisplayName("Should use request specification") void shouldUseRequestSpecification() { given() .spec(baseSpec) .when() .get("/users/1") .then() .statusCode(200); } } ``` ### Response Specification (Reusable Assertions) ```java @ExtendWith(SerenityJUnit5Extension.class) class ResponseSpecificationTest { private ResponseSpecification successSpec; private ResponseSpecification errorSpec; @BeforeEach void setup() { successSpec = new ResponseSpecBuilder() .expectStatusCode(200) .expectContentType("application/json") .expectResponseTime(lessThan(5000L)) .build(); errorSpec = new ResponseSpecBuilder() .expectStatusCode(404) .build(); } @Test @DisplayName("Should use success response specification") void shouldUseSuccessResponseSpec() { given() .baseUri("https://jsonplaceholder.typicode.com") .when() .get("/users/1") .then() .spec(successSpec) .body("name", notNullValue()); } } ``` ### Timeouts and Retries ```java import static io.restassured.config.HttpClientConfig.httpClientConfig; @Test @DisplayName("Should configure connection timeout") void shouldConfigureTimeout() { given() .baseUri("https://jsonplaceholder.typicode.com") .config(RestAssured.config() .httpClient(httpClientConfig() .setParam("http.connection.timeout", 5000) .setParam("http.socket.timeout", 5000) ) ) .when() .get("/users") .then() .statusCode(200); } ``` ### Logging ```java @Test @DisplayName("Should log request and response") void shouldLogRequestAndResponse() { given() .baseUri("https://jsonplaceholder.typicode.com") .log().all() // Log entire request .when() .get("/users/1") .then() .log().all() // Log entire response .statusCode(200); } @Test @DisplayName("Should log only on failure") void shouldLogOnlyOnFailure() { given() .baseUri("https://jsonplaceholder.typicode.com") .log().ifValidationFails() .when() .get("/users/1") .then() .log().ifValidationFails() .statusCode(200); } ``` --- ## Part 9: Hybrid UI + API Testing Combine UI and API testing for powerful end-to-end scenarios: ### Using Playwright with API Calls ```java import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright; import net.serenitybdd.screenplay.playwright.interactions.*; import net.serenitybdd.screenplay.rest.abilities.CallAnApi; import net.serenitybdd.screenplay.rest.interactions.Post; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Hybrid UI + API Tests") class HybridTest { @Test @DisplayName("Should create data via API and verify in UI") void shouldCreateDataViaApiAndVerifyInUi() { // Actor with both abilities Actor tester = Actor.named("Hybrid Tester") .whoCan(CallAnApi.at("https://api.example.com")) .whoCan(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration()); // Create data via API tester.attemptsTo( Post.to("/products") .with(request -> request .contentType("application/json") .body(""" {"name": "New Product", "price": 99.99} """) ) ); // Verify in UI tester.attemptsTo( Open.url("https://example.com/products"), Ensure.that(Text.of(".product-name")).contains("New Product") ); } @Test @DisplayName("Should perform UI action and verify via API") void shouldPerformUiActionAndVerifyViaApi() { Actor tester = Actor.named("Hybrid Tester") .whoCan(CallAnApi.at("https://api.example.com")) .whoCan(BrowseTheWebWithPlaywright.usingTheDefaultConfiguration()); // Perform UI action tester.attemptsTo( Open.url("https://example.com/checkout"), Click.on("#place-order-button") ); // Verify via API tester.attemptsTo( Get.resource("/orders/latest") ); tester.should( seeThatResponse(response -> response .statusCode(200) .body("status", equalTo("placed")) ) ); } } ``` --- ## Part 10: Best Practices ### 1. Organize by Domain ``` src/test/java/com/example/api/ ├── model/ │ ├── User.java │ ├── Post.java │ └── Booking.java ├── tasks/ │ ├── CreateUser.java │ ├── FetchUser.java │ └── Authenticate.java ├── questions/ │ ├── TheUser.java │ └── TheResponse.java ├── specs/ │ ├── RequestSpecs.java │ └── ResponseSpecs.java └── tests/ ├── UserApiTest.java ├── PostApiTest.java └── AuthenticationTest.java ``` ### 2. Use Meaningful Assertions ```java // Bad response.body("data", notNullValue()); // Good response.body("data.id", equalTo(expectedUserId)) .body("data.email", matchesPattern("^[\\w.-]+@[\\w.-]+\\.\\w+$")) .body("data.created_at", notNullValue()); ``` ### 3. Extract Common Setup ```java public class ApiTestBase { protected static final String BASE_URL = System.getProperty("api.baseurl", "https://api.example.com"); protected RequestSpecification baseRequest() { return given() .baseUri(BASE_URL) .contentType("application/json") .accept("application/json") .log().ifValidationFails(); } } ``` ### 4. Use Tags for Test Categories ```java @Tag("smoke") @Test void criticalEndpointTest() { } @Tag("regression") @Test void detailedValidationTest() { } @Tag("slow") @Test void performanceTest() { } ``` ### 5. Clean Up Test Data ```java @AfterEach void cleanup() { if (createdResourceId != null) { given() .baseUri(BASE_URL) .header("Authorization", "Bearer " + authToken) .when() .delete("/resources/" + createdResourceId) .then() .statusCode(anyOf(equalTo(200), equalTo(204), equalTo(404))); } } ``` --- ## Summary This tutorial covered: | Topic | Description | |-------|-------------| | Basic Operations | GET, POST, PUT, PATCH, DELETE requests | | Screenplay Pattern | Actor-centric API testing with tasks and questions | | Authentication | Basic auth, bearer tokens, API keys | | Error Handling | Testing error responses, schema validation | | Chained Calls | Using data from one call in another | | Data-Driven Tests | Parameterized tests with various data sources | | File Operations | Upload and download files | | Configuration | Request/response specs, timeouts, logging | | Hybrid Testing | Combining UI and API tests | ## Next Steps - Explore [Screenplay REST](/docs/screenplay/screenplay_rest) for more Screenplay patterns - Learn about [Serenity Reports](/docs/reporting/the_serenity_reports) for API test reporting - Check out [Cucumber Integration](/docs/cucumber/cucumber-junit5) for BDD-style API tests ## Resources - [Rest-Assured Documentation](https://rest-assured.io/) - [JSONPlaceholder API](https://jsonplaceholder.typicode.com/) - [ReqRes API](https://reqres.in/) - [Restful-Booker API](https://restful-booker.herokuapp.com/apidoc/) - [JSON Schema](https://json-schema.org/) --- # Getting Started With Cucumber using Serenity BDD and Screenplay :::tip Comprehensive Cucumber Guide Available This tutorial focuses on getting started with Cucumber and Screenplay. For comprehensive coverage of Cucumber configuration, JUnit 5 integration, parallel execution, and advanced topics, see the [Cucumber with JUnit 5](/docs/cucumber/cucumber-junit5) section. ::: ## Objectives At the end of this tutorial, you would have completed the following activities. 1. Write your first **specification by example** using [Gherkin](https://cucumber.io/docs/gherkin/reference/) language in a `.feature` file for the well-known [TodoMVC](http://todomvc.com) project 2. Make the specification (the `.feature` file from Step 1 above) executable using Serenity BDD and Cucumber with Screenplay pattern 3. Create a **living documentation** that also serves as a test report and a progress report ## Pre-requisites To run this tutorial, you will need a few things installed on your machine: * **Java**: Serenity BDD is a Java library, so you'll need a recent JDK installed. JDK 17 or higher should be fine. * **Maven**: You will need Maven 3 or higher installed in your computer. This acts as a build tool that also downloads the dependencies while building. * **A Java IDE**: You'll also need a Java Development Environment such as IntelliJ or Eclipse (and a working knowledge of Java). * **Git**: We'll be using a starter project on Github, and the sample code for this project lives on Github too, so I'll be assuming you have a basic understanding of Git. ## Creating your project The quickest way to start a new Serenity BDD with [Cucumber](https://cucumber.io/) project is to clone the starter project. For this tutorial, We will be using the **[Serenity BDD with Cucumber and Screenplay](https://github.com/serenity-bdd/serenity-cucumber-starter)** template project, which uses Serenity BDD and Cucumber 6.x. This project comes with a sample feature file already implemented for our reference. For now, we are going to ignore that and start writing a new feature file from scratch. :::info Just to make sure that the starter template's sample files do not interfere with our experience in this tutorial, **delete** the following files/directories. 1. Directory - `src/test/resources/features/search` 2. Directory - `src/test/java/starter/navigation` 3. Directory - `src/test/java/starter/search` 4. File - `src/test/java/starter/stepdefinitions/SearchStepDefinitions.java` ::: ## The project directory structure We will use some simple conventions to organise our feature files and the supporting Java classes, based on the standard Maven project structure outlined below: ``` src ├───main │ └───java │ └───starter └───test ├───java │ └───starter │ └───helpers │ └───stepdefinitions └───resources └───features ``` Here are some points to note about the directory structure. 1. Since we will test the publicly available [TodoMVC](http://todomvc.com) web application, we will not have any code in the `src/main` directory. 2. We will use the `src/test/resources/features` directory to store our `.feature` files, which are specifications that define the requirements. 3. We will use the `src/test/java/starter/stepdefinitions` directory to store the code implementing the steps mentioned in our `.feature` files. This code is called Glue code or the Step definitions. 4. We will use the `src/test/java/starter/helpers` directory to store the code for any helper classes that are needed by our step definitions. ## Writing the first feature file Now, let us start with writing a feature file to describe adding a new item to the to-do list. Create a new file with name `add_new_todo.feature` in the `src/test/resources/features` directory with the following content. ```gherkin Feature: Add new item to TODO list Scenario: Add buying milk to the list Given Rama is looking at his TODO list When he adds "Buy some milk" to the list Then he sees "Buy some milk" as an item in the TODO list ``` ## Writing the step definitions skeleton In order to translate the steps in the `add_new_todo.feature` into executable actions, we write Java classes called **Step Definitions**. Let us create a new file named `AddItemStepDefinitions.java` in the directory `src/test/java/starter/stepdefinitions` with the following skeleton content. Note that this is only a skeleton content. We will add flesh to this class later. ```java package starter.stepdefinitions; import io.cucumber.java.PendingException; import io.cucumber.java.en.Given; import io.cucumber.java.en.Then; import io.cucumber.java.en.When; import net.serenitybdd.screenplay.Actor; public class AddItemStepDefinitions { @Given("{actor} is looking at his TODO list") public void actor_is_looking_at_his_todo_list(Actor actor) { // Write code here that turns the phrase above into concrete actions throw new PendingException("Implement me"); } @When("{actor} adds {string} to the list") public void he_adds_to_the_list(Actor actor, String itemName) { // Write code here that turns the phrase above into concrete actions throw new PendingException("Implement me"); } @Then("{actor} sees {string} as an item in the TODO list") public void he_sees_as_an_item_in_the_todo_list(Actor actor, String expectedItemName) { // Write code here that turns the phrase above into concrete actions throw new PendingException("Implement me"); } } ``` The above file just throws exceptions whenever Cucumber tries to execute the steps and mark them as Pending. Let us try running the Maven build to see the result at this stage. We expect the build to fail stating that scenarios are pending to be implemented. Run the following command in a terminal or command prompt. ```bash mvn clean verify ``` Once the command completes, you can see output similar to the following. ``` [ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 7.255 s <<< FAILURE! - in starter.CucumberTestSuite [ERROR] Add new item to TODO list.Add buying milk to the list Time elapsed: 0.713 s <<< ERROR! io.cucumber.java.PendingException: TODO: implement me [INFO] [INFO] Results: [INFO] [ERROR] Errors: [ERROR] TODO: implement me [INFO] [ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0 ... ... ... [INFO] ----------------------------------------- [INFO] SERENITY TESTS: PENDING [INFO] ----------------------------------------- [INFO] | Test cases executed | 1 [INFO] | Tests executed | 1 [INFO] | Tests passed | 0 [INFO] | Tests failed | 0 [INFO] | Tests with errors | 0 [INFO] | Tests compromised | 0 [INFO] | Tests aborted | 0 [INFO] | Tests pending | 1 [INFO] | Tests ignored/skipped | 0 [INFO] ------------------------ | -------------- [INFO] | Total Duration | 365ms [INFO] | Fastest test took | 365ms [INFO] | Slowest test took | 365ms [INFO] ----------------------------------------- ... ... ... [INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (default) @ cucumber-starter --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD FAILURE [INFO] ------------------------------------------------------------------------ [INFO] Total time: 30.465 s [INFO] Finished at: 2022-08-12T14:52:57+05:30 [INFO] ------------------------------------------------------------------------ ``` The above output is as we expected. The build is failing with a `PendingException` and the tests are marked as pending. ## Creating Helper Classes for Step Definitions So far, we have only dummy step definitions. Let us implement real tests now. In order to implement the real tests, let us create a few helper classes. #### Page Object Let us first create a file `TodoListPage.java` in the directory `src/test/java/starter/helpers` with the following content. ```java package starter.helpers; import net.serenitybdd.core.pages.PageObject; import net.serenitybdd.screenplay.targets.Target; import net.serenitybdd.annotations.DefaultUrl; @DefaultUrl("https://todomvc.com/examples/angularjs/#/") public class TodoListPage extends PageObject { public static Target ITEM_NAME_FIELD = Target.the("item name field").locatedBy(".new-todo"); public static Target ITEMS_LIST = Target.the(" item list").locatedBy(".todo-list li"); } ``` This class is what we call as a `PageObject`. This contains all the information we will need to use a particular web page, the TODO app, in this case. The `@DefaultUrl` annotation specifies the URL that needs to be typed in the browser's address bar to access this page. There are two static fields `ITEM_NAME_FIELD` and `ITEMS_LIST` that help identify specific HTML elements on the page, which we will use later in our step definition files. #### Navigation Helper Let us create a file `NavigateTo.java` in the directory `src/test/java/starter/helpers` with the following content. ```java package starter.helpers; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; public class NavigateTo { public static Performable theTodoListPage() { return Task.where("{0} opens the Todo list page", Open.browserOn().the(TodoListPage.class)); } } ``` The above class uses Serenity BDD's [Screenplay pattern](/docs/screenplay/screenplay_fundamentals) to describe the behavior in a lucid manner. This class helps us to open the browser with the proper URL. #### Action Definition Next, let us create a file `AddAnItem.java` in the directory `src/test/java/starter/helpers` with the following content. ```java package starter.helpers; import net.serenitybdd.screenplay.Performable; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Enter; import org.openqa.selenium.Keys; public class AddAnItem { public static Performable withName(String itemName){ return Task.where("{0} adds an item with name "+itemName, Enter.theValue(itemName) .into(TodoListPage.ITEM_NAME_FIELD) .thenHit(Keys.ENTER) ); } } ``` The above code explains the steps needed to add an item to the list, i.e., typing the item name in the text box and pressing the ENTER key. ## Adding Details to Step Definitions Now that our helper classes are ready, we can add real details to the step definitions present in `AddItemStepDefinitions.java` Open the `AddItemStepDefinitions.java` file (we already created this file) and edit it to have the following content. ```java package starter.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 starter.helpers.AddAnItem; import starter.helpers.NavigateTo; import starter.helpers.TodoListPage; public class AddItemStepDefinitions { @Given("{actor} is looking at his TODO list") public void actor_is_looking_at_his_todo_list(Actor actor) { actor.wasAbleTo(NavigateTo.theTodoListPage()); } @When("{actor} adds {string} to the list") public void he_adds_to_the_list(Actor actor, String itemName) { actor.attemptsTo(AddAnItem.withName(itemName)); } @Then("{actor} sees {string} as an item in the TODO list") public void he_sees_as_an_item_in_the_todo_list(Actor actor, String expectedItemName) { actor.attemptsTo(Ensure.that(TodoListPage.ITEMS_LIST).hasText(expectedItemName)); } } ``` Notice how the code reads like the spoken English. This is one of the pleasant side-effects of using Screenplay pattern in your Cucumber step definitions. ## Running the build again Now, let us run the build again by issuing the following command from the terminal or command line. ```bash mvn clean verify ``` Now, you will see the following output. ``` [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 28.42 s - in starter.CucumberTestSuite [INFO] [INFO] Results: [INFO] [INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0 ... ... ... [INFO] ----------------------------------------- [INFO] SERENITY TESTS: SUCCESS [INFO] ----------------------------------------- [INFO] | Test cases executed | 1 [INFO] | Tests executed | 1 [INFO] | Tests passed | 1 [INFO] | Tests failed | 0 [INFO] | Tests with errors | 0 [INFO] | Tests compromised | 0 [INFO] | Tests aborted | 0 [INFO] | Tests pending | 0 [INFO] | Tests ignored/skipped | 0 [INFO] ------------------------ | -------------- [INFO] | Total Duration | 20s 001ms [INFO] | Fastest test took | 20s 001ms [INFO] | Slowest test took | 20s 001ms [INFO] ----------------------------------------- [INFO] [INFO] SERENITY REPORTS [INFO] - Full Report: file:///C:/Users/calib/source-codes/temp/serenity-cucumber-starter/target/site/serenity/index.html [INFO] [INFO] --- maven-failsafe-plugin:3.0.0-M5:verify (default) @ cucumber-starter --- [INFO] ------------------------------------------------------------------------ [INFO] BUILD SUCCESS [INFO] ------------------------------------------------------------------------ [INFO] Total time: 49.894 s [INFO] Finished at: 2022-08-12T15:28:52+05:30 [INFO] ------------------------------------------------------------------------ ``` Yes, the tests passed and the build is successful now. We have managed to test our feature successfully 🎉 ## Reporting and Living Documentation If you notice carefully, the output of the `mvn clean verify` command informed us that a report is created in `target/site/serenity/index.html` When you open this file in a web browser, you see a beautiful report like this. ![Report Home Page](img/cucumber-test-report-home.png) You can also find the feature-wise results detailing the scenarios in the `Features` tab. ![Report of Features](img/cucumber-report-features.png) Feel free to navigate the links in this report and look around. This is also called a **Living Documentation** of the product because, this is generated by actually executing the specifications, rather than just writing it as a wiki or a document stored in cloud. As the product evolves, the scenarios will get added and this report is the single source of truth about what works and what is pending to be implemented in the product. In some cases, teams use this document to onboard new joiners to the team. If you are feeling adventurous, this document can also be used as a user guide. ## Next Steps In this tutorial, we just touched the surface of using Serenity BDD with Cucumber. There are multiple ways to customize the report, organize the feature files, implement the step definitions and so on. Refer to the links in the user manual to know more about further possibilities. --- # Data-Driven Testing with Serenity BDD Data-driven testing allows you to run the same test logic with multiple sets of input data, making your tests more comprehensive and maintainable. This guide covers data-driven testing approaches with both JUnit 5 and Cucumber. ## Prerequisites - Java 17 or higher - Serenity BDD 5.3.1 or higher - JUnit 5 6.0.1 or higher (for JUnit examples) - Cucumber 7.33.0 or higher (for Cucumber examples) ## Why Data-Driven Testing? Data-driven testing helps you: - **Test multiple scenarios** with minimal code duplication - **Improve test coverage** by testing edge cases and boundary values - **Separate test logic from test data** for better maintainability - **Make tests more readable** by clearly showing what varies between test cases ## JUnit 5 Parameterized Tests JUnit 5 provides powerful parameterized testing capabilities through `@ParameterizedTest`. ### Basic Setup First, add the JUnit Jupiter parameters dependency: ```xml org.junit.jupiter junit-jupiter-params 6.0.1 test ``` ### Simple Value Sources Use `@ValueSource` for simple data types: ```java import net.serenitybdd.annotations.Managed; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.extension.ExtendWith; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; import org.openqa.selenium.WebDriver; @ExtendWith(SerenityJUnit5Extension.class) class SearchTests { @Managed(driver = "chrome", options = "headless") WebDriver driver; @ParameterizedTest @ValueSource(strings = {"Selenium", "Cucumber", "JUnit 5"}) void shouldFindSearchResults(String searchTerm) { // Navigate to search page driver.get("https://duckduckgo.com"); // Perform search with each term SearchPage searchPage = new SearchPage(driver); searchPage.searchFor(searchTerm); // Verify results assertThat(searchPage.getResults()).isNotEmpty(); } } ``` **Supported types for @ValueSource:** - `strings` - String values - `ints` - Integer values - `longs` - Long values - `doubles` - Double values - `booleans` - Boolean values ### CSV Source - Inline Data Use `@CsvSource` for multiple parameters per test case: ```java @ExtendWith(SerenityJUnit5Extension.class) class LoginTests { @Managed(driver = "chrome", options = "headless") WebDriver driver; @Steps LoginActions loginActions; @ParameterizedTest @CsvSource({ "standard_user, secret_sauce, true", "locked_out_user, secret_sauce, false", "problem_user, secret_sauce, true", "invalid_user, wrong_password, false" }) void shouldHandleLoginScenarios(String username, String password, boolean shouldSucceed) { loginActions.navigateToLoginPage(); loginActions.loginWith(username, password); if (shouldSucceed) { loginActions.shouldSeeProductsPage(); } else { loginActions.shouldSeeErrorMessage(); } } } ``` **Tips for @CsvSource:** - Values are comma-separated by default - Use single quotes for strings containing commas: `'value, with, commas'` - Empty values are treated as null - Whitespace is trimmed by default ### CSV Source with Custom Delimiter ```java @ParameterizedTest @CsvSource(delimiter = '|', value = { "Alice | alice@example.com | Premium", "Bob | bob@example.com | Standard", "Carol | carol@example.com | Basic" }) void shouldHandleDifferentUserTypes(String name, String email, String accountType) { // Test implementation } ``` ### CSV File Source - External Data Store test data in external CSV files for better maintainability: **src/test/resources/test-data/users.csv:** ```csv username,password,email,expectedRole admin,admin123,admin@example.com,ADMIN user1,password1,user1@example.com,USER user2,password2,user2@example.com,USER guest,guest123,guest@example.com,GUEST ``` ```java @ExtendWith(SerenityJUnit5Extension.class) class UserRegistrationTests { @Steps RegistrationActions registration; @ParameterizedTest @CsvFileSource(resources = "/test-data/users.csv", numLinesToSkip = 1) void shouldRegisterUsers(String username, String password, String email, String expectedRole) { registration.registerNewUser(username, password, email); registration.shouldHaveRole(expectedRole); } } ``` **@CsvFileSource options:** - `resources` - Path to CSV file (relative to src/test/resources) - `numLinesToSkip` - Skip header row (typically 1) - `delimiter` - Custom delimiter (default is comma) - `encoding` - File encoding (default is UTF-8) ### Method Source - Complex Objects Use `@MethodSource` for complex test data or when you need more control: ```java @ExtendWith(SerenityJUnit5Extension.class) class ProductTests { @Steps ShoppingActions shopping; @ParameterizedTest @MethodSource("productTestData") void shouldAddProductToCart(Product product, int quantity, double expectedTotal) { shopping.addToCart(product, quantity); shopping.shouldShowTotal(expectedTotal); } static Stream productTestData() { return Stream.of( Arguments.of(new Product("Laptop", 999.99), 1, 999.99), Arguments.of(new Product("Mouse", 29.99), 2, 59.98), Arguments.of(new Product("Keyboard", 79.99), 3, 239.97) ); } } ``` ### Method Source from External Class Organize test data in separate classes: ```java class TestDataProviders { static Stream loginScenarios() { return Stream.of( Arguments.of("admin", "admin123", true, "Dashboard"), Arguments.of("user", "user123", true, "Home"), Arguments.of("invalid", "wrong", false, "Login Error") ); } static Stream searchQueries() { return Stream.of( Arguments.of("Selenium", 10), Arguments.of("Cucumber", 8), Arguments.of("JUnit", 15) ); } } @ExtendWith(SerenityJUnit5Extension.class) class LoginTests { @ParameterizedTest @MethodSource("com.example.TestDataProviders#loginScenarios") void shouldHandleLoginScenarios(String username, String password, boolean shouldSucceed, String expectedPage) { // Test implementation } } ``` ### Enum Source Test with enum values: ```java enum UserRole { ADMIN, MANAGER, USER, GUEST } @ExtendWith(SerenityJUnit5Extension.class) class RoleBasedTests { @ParameterizedTest @EnumSource(UserRole.class) void shouldHandleAllUserRoles(UserRole role) { // Test each role } @ParameterizedTest @EnumSource(value = UserRole.class, names = {"ADMIN", "MANAGER"}) void shouldAllowAdminActions(UserRole role) { // Test only admin and manager roles } } ``` ### Custom Display Names Make test reports more readable: ```java @ParameterizedTest(name = "Search for ''{0}'' should return at least {1} results") @CsvSource({ "Selenium, 10", "Cucumber, 5", "JUnit, 8" }) void searchTests(String term, int minimumResults) { // Test implementation } ``` The `{0}`, `{1}` placeholders correspond to parameter positions. ## Cucumber Scenario Outlines Cucumber provides Scenario Outlines for data-driven BDD tests. ### Basic Scenario Outline **features/login.feature:** ```gherkin Feature: User Login Scenario Outline: Login with different credentials Given the user is on the login page When the user logs in with username "" and password "" Then the login should And the user should see the "" page Examples: | username | password | result | page | | standard_user | secret_sauce | succeed | Products | | locked_user | secret_sauce | fail | Login | | invalid_user | wrong_pass | fail | Login | ``` ### Step Definitions with Screenplay ```java import io.cucumber.java.en.*; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.abilities.BrowseTheWeb; import net.serenitybdd.screenplay.actions.Open; import static net.serenitybdd.screenplay.GivenWhenThen.*; public class LoginStepDefinitions { @Given("{actor} is on the login page") public void userIsOnLoginPage(Actor actor) { actor.attemptsTo( Open.url("https://www.saucedemo.com") ); } @When("{actor} logs in with username {string} and password {string}") public void userLogsIn(Actor actor, String username, String password) { actor.attemptsTo( Login.withCredentials(username, password) ); } @Then("the login should {word}") public void verifyLoginResult(String result) { // Verification based on result } @Then("{actor} should see the {string} page") public void verifiesPage(Actor actor, String expectedPage) { actor.should( seeThat(TheCurrentPage.title(), containsString(expectedPage)) ); } } ``` ### Multiple Examples Tables Use multiple Examples tables to organize different test scenarios: ```gherkin Feature: Shopping Cart Scenario Outline: Add products to cart Given the user is logged in When the user adds "" with quantity Then the cart total should be Examples: Single items | product | quantity | total | | Laptop | 1 | 999.99 | | Mouse | 1 | 29.99 | | Keyboard | 1 | 79.99 | Examples: Multiple quantities | product | quantity | total | | Mouse | 3 | 89.97 | | Keyboard | 2 | 159.98 | ``` ### Data Tables For more complex data structures, use Data Tables: ```gherkin Feature: Bulk User Registration Scenario: Register multiple users Given the admin is logged in When the admin registers the following users: | username | email | role | | alice | alice@example.com | Admin | | bob | bob@example.com | User | | charlie | charlie@example.com| Manager | Then all users should be created successfully ``` Step definition: ```java @When("the admin registers the following users:") public void registerUsers(DataTable dataTable) { List> users = dataTable.asMaps(); for (Map user : users) { String username = user.get("username"); String email = user.get("email"); String role = user.get("role"); actor.attemptsTo( RegisterUser.withDetails(username, email, role) ); } } ``` ### External Data Files with Cucumber Load data from external files: ```gherkin Scenario: Register users from CSV Given the admin is logged in When the admin imports users from "test-data/users.csv" Then all users should be registered ``` ```java @When("the admin imports users from {string}") public void importUsersFromFile(String filePath) { List users = UserDataLoader.fromCsv(filePath); users.forEach(user -> actor.attemptsTo(RegisterUser.with(user)) ); } ``` ## External Data Sources ### Reading from CSV Files Create a reusable CSV reader: ```java public class CsvDataReader { public static List> readCsv(String filePath) { List> data = new ArrayList<>(); try (BufferedReader reader = new BufferedReader( new FileReader(filePath))) { String headerLine = reader.readLine(); String[] headers = headerLine.split(","); String line; while ((line = reader.readLine()) != null) { String[] values = line.split(","); Map row = new HashMap<>(); for (int i = 0; i < headers.length; i++) { row.put(headers[i].trim(), values[i].trim()); } data.add(row); } } catch (IOException e) { throw new RuntimeException("Failed to read CSV file: " + filePath, e); } return data; } } ``` ### Reading from JSON Files Use Jackson or Gson to read JSON test data: **src/test/resources/test-data/products.json:** ```json [ { "name": "Laptop", "price": 999.99, "category": "Electronics", "inStock": true }, { "name": "Mouse", "price": 29.99, "category": "Accessories", "inStock": true } ] ``` ```java import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.core.type.TypeReference; public class JsonDataReader { private static final ObjectMapper mapper = new ObjectMapper(); public static List readJson(String filePath, Class type) { try { InputStream stream = JsonDataReader.class .getResourceAsStream(filePath); return mapper.readValue(stream, mapper.getTypeFactory().constructCollectionType(List.class, type)); } catch (IOException e) { throw new RuntimeException("Failed to read JSON file: " + filePath, e); } } } // Usage @MethodSource("productData") static Stream productData() { List products = JsonDataReader.readJson( "/test-data/products.json", Product.class ); return products.stream(); } ``` ### Reading from Excel Files Use Apache POI for Excel files: ```xml org.apache.poi poi-ooxml 5.2.3 test ``` ```java import org.apache.poi.ss.usermodel.*; import org.apache.poi.xssf.usermodel.XSSFWorkbook; public class ExcelDataReader { public static List> readExcel(String filePath, String sheetName) { List> data = new ArrayList<>(); try (FileInputStream fis = new FileInputStream(filePath); Workbook workbook = new XSSFWorkbook(fis)) { Sheet sheet = workbook.getSheet(sheetName); Row headerRow = sheet.getRow(0); for (int i = 1; i <= sheet.getLastRowNum(); i++) { Row row = sheet.getRow(i); Map rowData = new HashMap<>(); for (int j = 0; j < headerRow.getLastCellNum(); j++) { String header = headerRow.getCell(j).getStringCellValue(); String value = getCellValueAsString(row.getCell(j)); rowData.put(header, value); } data.add(rowData); } } catch (IOException e) { throw new RuntimeException("Failed to read Excel file", e); } return data; } private static String getCellValueAsString(Cell cell) { if (cell == null) return ""; switch (cell.getCellType()) { case STRING: return cell.getStringCellValue(); case NUMERIC: return String.valueOf(cell.getNumericCellValue()); case BOOLEAN: return String.valueOf(cell.getBooleanCellValue()); default: return ""; } } } ``` ## Test Data Builders Use the Builder pattern for complex test data: ```java public class UserBuilder { private String username; private String email; private String password; private String role = "USER"; private boolean active = true; public static UserBuilder aUser() { return new UserBuilder(); } public UserBuilder withUsername(String username) { this.username = username; return this; } public UserBuilder withEmail(String email) { this.email = email; return this; } public UserBuilder withPassword(String password) { this.password = password; return this; } public UserBuilder withRole(String role) { this.role = role; return this; } public UserBuilder inactive() { this.active = false; return this; } public User build() { return new User(username, email, password, role, active); } } // Usage @MethodSource("userTestData") static Stream userTestData() { return Stream.of( aUser().withUsername("admin").withRole("ADMIN").build(), aUser().withUsername("user1").withEmail("user1@example.com").build(), aUser().withUsername("inactive").inactive().build() ); } ``` ## Using Faker for Random Data Generate realistic random test data: ```xml com.github.javafaker javafaker 1.0.2 test ``` ```java import com.github.javafaker.Faker; public class TestDataGenerator { private static final Faker faker = new Faker(); public static User randomUser() { return aUser() .withUsername(faker.name().username()) .withEmail(faker.internet().emailAddress()) .withPassword(faker.internet().password(8, 16)) .build(); } public static Product randomProduct() { return new Product( faker.commerce().productName(), Double.parseDouble(faker.commerce().price()), faker.commerce().department() ); } } // Usage in tests @ParameterizedTest @MethodSource("randomUsers") void shouldRegisterRandomUsers(User user) { registration.registerUser(user); registration.shouldSucceed(); } static Stream randomUsers() { return Stream.generate(TestDataGenerator::randomUser).limit(10); } ``` ## Best Practices ### 1. Keep Test Data Close to Tests ``` src/test/resources/ ├── test-data/ │ ├── users/ │ │ ├── valid-users.csv │ │ └── invalid-users.csv │ ├── products/ │ │ └── products.json │ └── shared/ │ └── countries.csv ``` ### 2. Use Meaningful Test Names ```java @ParameterizedTest(name = "User ''{0}'' with role ''{1}'' should access ''{2}''") @CsvSource({ "admin, ADMIN, dashboard", "user, USER, homepage" }) void accessControlTests(String username, String role, String page) { // Test implementation } ``` ### 3. Validate Test Data ```java @MethodSource("userData") static Stream userData() { return loadUsersFromCsv("/test-data/users.csv") .stream() .peek(user -> { assertThat(user.getEmail()).contains("@"); assertThat(user.getPassword()).hasSizeGreaterThan(8); }); } ``` ### 4. Isolate Test Data Ensure each test has its own independent data: ```java @BeforeEach void setUp() { // Clean up any existing test data testDataCleanup.deleteTestUsers(); } @ParameterizedTest @MethodSource("userTestData") void userTests(User user) { // Each test gets fresh data String uniqueUsername = user.getUsername() + "_" + UUID.randomUUID(); User isolatedUser = user.withUsername(uniqueUsername); // Test with isolated data } ``` ### 5. Document Data Requirements ```java /** * Tests user registration with various valid inputs. * * Test data requirements: * - Username: 3-20 characters, alphanumeric * - Email: Valid email format * - Password: Minimum 8 characters * - Role: One of [USER, ADMIN, MANAGER] */ @ParameterizedTest @CsvFileSource(resources = "/test-data/valid-users.csv") void shouldRegisterValidUsers(String username, String email, String password, String role) { // Test implementation } ``` ### 6. Separate Valid and Invalid Data ```java // Valid data tests @ParameterizedTest @CsvFileSource(resources = "/test-data/valid-users.csv") void shouldAcceptValidUsers(User user) { // Expect success } // Invalid data tests @ParameterizedTest @CsvFileSource(resources = "/test-data/invalid-users.csv") void shouldRejectInvalidUsers(User user, String expectedError) { // Expect specific error } ``` ## Complete Example: E-Commerce Testing Here's a complete example combining multiple techniques: **Product.java:** ```java public class Product { private final String name; private final double price; private final String category; private final boolean inStock; // Constructor, getters, builder... } ``` **test-data/products.csv:** ```csv name,price,category,inStock,expectedInCart Laptop,999.99,Electronics,true,true Headphones,79.99,Audio,true,true Out of Stock Item,49.99,Misc,false,false ``` **ShoppingTests.java:** ```java @ExtendWith(SerenityJUnit5Extension.class) class ShoppingTests { @Steps ShoppingActions shopping; @ParameterizedTest(name = "Adding {0} (${1}) should {4}") @CsvFileSource(resources = "/test-data/products.csv", numLinesToSkip = 1) void shouldHandleProductAddition(String name, double price, String category, boolean inStock, boolean expectedInCart) { Product product = new Product(name, price, category, inStock); shopping.addProductToCart(product); if (expectedInCart) { shopping.shouldSeeProductInCart(product); shopping.cartTotalShouldBe(price); } else { shopping.shouldSeeOutOfStockMessage(); } } } ``` ## Troubleshooting ### Issue: Parameterized tests not running **Solution:** Ensure you have the junit-jupiter-params dependency and use `@ParameterizedTest` not `@Test`. ### Issue: CSV data not loading **Solution:** - Check file path is relative to `src/test/resources` - Verify `numLinesToSkip` is set correctly - Check for proper CSV formatting ### Issue: Data appearing in wrong order **Solution:** JUnit doesn't guarantee execution order. Use `@TestMethodOrder` if needed: ```java @TestMethodOrder(OrderAnnotation.class) class OrderedTests { @ParameterizedTest @Order(1) @ValueSource(strings = {"first"}) void test1(String value) { } } ``` ## Next Steps - Explore [Screenplay Pattern](/docs/screenplay/screenplay_fundamentals) for better test organization - Learn about [Parallel Execution](/docs/junit5/parallel-execution) to speed up data-driven tests - Check out [Serenity Reports](/docs/reporting/the_serenity_reports) to see how parameterized tests appear in reports ## Additional Resources - [JUnit 5 Parameterized Tests Guide](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests) - [Cucumber Scenario Outlines](https://cucumber.io/docs/gherkin/reference/#scenario-outline) - [JavaFaker Documentation](https://github.com/DiUS/java-faker) --- # Migrating From Serenity BDD 3.x to Serenity BDD 4.x ## Introduction Serenity BDD 4.0.0 is a significant update that aligns with modern testing libraries by supporting JDK 17. This major release includes changes to package structures, reflecting adjustments in the module structure for JDK 17. If you are migrating from Serenity 3.x to Serenity 4.x, this tutorial will guide you through the necessary steps to make your project compatible with the latest version. ## Step 1: Ensure JDK 17 Compatibility Make sure your project is compatible with JDK 17, as Serenity BDD 4.x requires it as a minimum version. If you are using Maven, update your project's `pom.xml` file to include the `maven-compiler-plugin` configured for JDK 17. Here's an example: ```xml org.apache.maven.plugins maven-compiler-plugin 3.8.1 11 11 ``` If your project uses Gradle, you'll need to specify the Java version in your `build.gradle` file to ensure compatibility with JDK 17. Here's an example: ```groovy plugins { id 'java' } sourceCompatibility = '11' targetCompatibility = '11' ``` ## Step 2: Update Serenity Dependencies Update the Serenity BDD dependencies in your project's build file to the latest 4.x version, e.g. ```xml 4.0.0 net.serenity-bdd serenity-core ${serenity.version} net.serenity-bdd serenity-junit ${serenity.version} net.serenity-bdd serenity-screenplay ${serenity.version} net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} net.serenity-bdd serenity-ensure ${serenity.version} net.serenity-bdd serenity-cucumber ${serenity.version} ``` ## Step 3: Modify Annotations Because of Java 9 module constraints, some of the packages have changed places in the new version. Most importantly, all of the main Serenity annotations can now be found in the `net.serenitybdd.annotations` package. In your project, replace the main Serenity annotations, such as `@Step`, to the new package `net.serenitybdd.annotations` from the old package `net.thucydides.core.annotations`. If your project is large, you can update all of your annotations in one go like this: ![](img/replace-serenity-annotations.png) ## Step 4: Update Package References If you are using internal Serenity classes, you may need to modify the relevant package references in your code based on the following table: | Module | Old packages | New Packages | | -------- | ------------ | ------------ | | serenity-screenplay-webdriver | net.serenity.screenplay.* | net.serenity.screenplay.webdriver.* | | | net.serenitybdd.screenplay.webtest.actions.* | net.serenitybdd.screenplay.webdriver.actions.* | | serenity-model | net.serenitybdd.core.* | net.serenitybdd.model.* | | | net.thucydides.core.annotations.* | net.serenitybdd.annotations.* | | | Other net.thucydides.core.* | net.thucydides.model.* | For example, you may need to replace `net.thucydides.core.util.EnvironmentVariables` with `net.thucydides.model.util.EnvironmentVariables` ## Step 5: Test Your Changes After making these changes, run your test suite to verify that everything is working correctly with the new Serenity BDD version. Pay particular attention to the following: - **Requirements Hierarchy:** Ensure that the requirements hierarchy is displaying correctly in your reports. - **Test Execution:** Confirm that all your tests are being executed as expected, without any missing or failing tests that previously succeeded. ## Conclusion Migrating to Serenity BDD 4.x involves updating package references to align with the new JDK 17 module structure. Follow the steps above, and consult the official Serenity BDD documentation for additional information or support. Remember to thoroughly test your updated project, particularly focusing on the requirements hierarchy and test execution, to ensure that the migration has been successful. --- # CI/CD Integration with Serenity BDD This guide shows you how to integrate Serenity BDD tests into popular CI/CD platforms, including GitHub Actions, Jenkins, and GitLab CI. Learn how to run tests automatically, publish reports, and get fast feedback on your code changes. ## Prerequisites - Serenity BDD project configured with Maven or Gradle - Version control repository (Git) - Access to a CI/CD platform ## Quick Start Checklist Before setting up CI/CD, ensure your project: - ✅ Runs successfully locally with `mvn clean verify` or `gradle clean test` - ✅ Has all dependencies properly configured - ✅ Uses headless browser mode for web tests - ✅ Has reasonable test execution timeouts - ✅ Generates Serenity reports in `target/site/serenity` (Maven) or `build/reports/serenity` (Gradle) ## GitHub Actions GitHub Actions is the easiest way to set up CI/CD for projects hosted on GitHub. ### Basic Setup Create `.github/workflows/serenity-tests.yml` in your repository: ```yaml name: Serenity BDD Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'maven' - name: Run Serenity tests run: mvn clean verify - name: Publish Serenity Report if: always() uses: actions/upload-artifact@v4 with: name: serenity-report path: target/site/serenity retention-days: 30 ``` ### Gradle Version For Gradle projects: ```yaml name: Serenity BDD Tests on: push: branches: [ main, develop ] pull_request: branches: [ main, develop ] jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'gradle' - name: Run Serenity tests run: ./gradlew clean test aggregate - name: Publish Serenity Report if: always() uses: actions/upload-artifact@v4 with: name: serenity-report path: build/reports/serenity retention-days: 30 ``` ### Publishing Reports to GitHub Pages Automatically publish Serenity reports as a website: ```yaml name: Serenity Tests with GitHub Pages on: push: branches: [ main ] permissions: contents: write pages: write id-token: write jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'maven' - name: Run Serenity tests run: mvn clean verify - name: Deploy to GitHub Pages if: always() uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/site/serenity destination_dir: reports/${{ github.run_number }} ``` **Access your reports at:** `https://.github.io//reports/` ### Parallel Execution in GitHub Actions Run tests in parallel using a matrix strategy: ```yaml name: Serenity Parallel Tests on: push: branches: [ main ] jobs: test: runs-on: ubuntu-latest strategy: matrix: browser: [chrome, firefox, edge] test-suite: [smoke, regression] fail-fast: false steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'maven' - name: Run tests run: mvn verify -Dwebdriver.driver=${{ matrix.browser }} -Dtags=${{ matrix.test-suite }} - name: Upload Report if: always() uses: actions/upload-artifact@v4 with: name: report-${{ matrix.browser }}-${{ matrix.test-suite }} path: target/site/serenity ``` ### Advanced Configuration with Environment Variables ```yaml name: Serenity Tests - All Environments on: push: branches: [ main ] workflow_dispatch: inputs: environment: description: 'Environment to test' required: true type: choice options: - dev - staging - production jobs: test: runs-on: ubuntu-latest steps: - name: Checkout code uses: actions/checkout@v4 - name: Set up JDK 17 uses: actions/setup-java@v4 with: java-version: '17' distribution: 'temurin' cache: 'maven' - name: Run tests env: BASE_URL: ${{ secrets[format('{0}_BASE_URL', github.event.inputs.environment || 'dev')] }} API_KEY: ${{ secrets[format('{0}_API_KEY', github.event.inputs.environment || 'dev')] }} TEST_USER: ${{ secrets.TEST_USER }} TEST_PASSWORD: ${{ secrets.TEST_PASSWORD }} run: | mvn clean verify \ -Dwebdriver.base.url=$BASE_URL \ -Dapi.key=$API_KEY \ -Dtest.user=$TEST_USER \ -Dtest.password=$TEST_PASSWORD - name: Upload Report if: always() uses: actions/upload-artifact@v4 with: name: serenity-report-${{ github.event.inputs.environment || 'dev' }} path: target/site/serenity ``` ### Notifications Send Slack notifications on test results: ```yaml - name: Notify Slack on Success if: success() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK }} payload: | { "text": "✅ Serenity Tests Passed - Build #${{ github.run_number }}" } - name: Notify Slack on Failure if: failure() uses: slackapi/slack-github-action@v1 with: webhook-url: ${{ secrets.SLACK_WEBHOOK }} payload: | { "text": "❌ Serenity Tests Failed - Build #${{ github.run_number }}\nView: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}" } ``` ## Jenkins Jenkins is a popular self-hosted CI/CD platform with extensive plugin support. ### Declarative Pipeline Create a `Jenkinsfile` in your repository root: ```groovy pipeline { agent any tools { maven 'Maven-3.9' jdk 'JDK-17' } stages { stage('Checkout') { steps { checkout scm } } stage('Build and Test') { steps { sh 'mvn clean verify' } } stage('Publish Reports') { steps { publishHTML([ reportDir: 'target/site/serenity', reportFiles: 'index.html', reportName: 'Serenity Report', keepAll: true, alwaysLinkToLastBuild: true, allowMissing: false ]) } } } post { always { junit '**/target/failsafe-reports/*.xml' archiveArtifacts artifacts: 'target/site/serenity/**/*', allowEmptyArchive: true } failure { emailext( subject: "Failed: ${currentBuild.fullDisplayName}", body: "Test execution failed. Check console output at ${env.BUILD_URL}", to: 'team@example.com' ) } } } ``` ### Gradle Pipeline ```groovy pipeline { agent any tools { gradle 'Gradle-8' jdk 'JDK-17' } stages { stage('Checkout') { steps { checkout scm } } stage('Build and Test') { steps { sh './gradlew clean test aggregate' } } stage('Publish Reports') { steps { publishHTML([ reportDir: 'build/reports/serenity', reportFiles: 'index.html', reportName: 'Serenity Report', keepAll: true, alwaysLinkToLastBuild: true ]) } } } post { always { junit '**/build/test-results/test/*.xml' } } } ``` ### Parallel Execution in Jenkins Run tests in parallel across multiple nodes: ```groovy pipeline { agent none stages { stage('Parallel Tests') { parallel { stage('Chrome Tests') { agent { label 'chrome' } steps { sh 'mvn verify -Dwebdriver.driver=chrome' } } stage('Firefox Tests') { agent { label 'firefox' } steps { sh 'mvn verify -Dwebdriver.driver=firefox' } } stage('Edge Tests') { agent { label 'edge' } steps { sh 'mvn verify -Dwebdriver.driver=edge' } } } } stage('Aggregate Reports') { agent any steps { publishHTML([ reportDir: 'target/site/serenity', reportFiles: 'index.html', reportName: 'Consolidated Serenity Report' ]) } } } } ``` ### Jenkins Configuration with Parameters ```groovy pipeline { agent any parameters { choice( name: 'ENVIRONMENT', choices: ['dev', 'staging', 'production'], description: 'Environment to test' ) choice( name: 'BROWSER', choices: ['chrome', 'firefox', 'edge'], description: 'Browser to use' ) string( name: 'TAGS', defaultValue: '', description: 'Tags to filter tests (e.g., @smoke, @regression)' ) } stages { stage('Test') { steps { script { def baseUrl = "" switch(params.ENVIRONMENT) { case 'dev': baseUrl = 'https://dev.example.com' break case 'staging': baseUrl = 'https://staging.example.com' break case 'production': baseUrl = 'https://www.example.com' break } sh """ mvn clean verify \ -Dwebdriver.driver=${params.BROWSER} \ -Dwebdriver.base.url=${baseUrl} \ -Dcucumber.filter.tags='${params.TAGS}' """ } } } } } ``` ### Installing Jenkins Plugins Required plugins: ```bash # Install via Jenkins CLI or UI - HTML Publisher Plugin - JUnit Plugin - Pipeline Plugin - Git Plugin - Email Extension Plugin ``` ## GitLab CI GitLab CI is integrated directly into GitLab repositories. ### Basic Configuration Create `.gitlab-ci.yml` in your repository root: ```yaml image: maven:3.9-eclipse-temurin-17 stages: - test - report variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" cache: paths: - .m2/repository test: stage: test script: - mvn clean verify artifacts: when: always paths: - target/site/serenity expire_in: 30 days reports: junit: - target/failsafe-reports/TEST-*.xml pages: stage: report script: - mkdir -p public - cp -r target/site/serenity/* public/ artifacts: paths: - public only: - main ``` **Reports will be available at:** `https://.gitlab.io//` ### Gradle Version ```yaml image: gradle:8-jdk17 stages: - test - report variables: GRADLE_OPTS: "-Dorg.gradle.daemon=false" cache: paths: - .gradle test: stage: test script: - gradle clean test aggregate artifacts: when: always paths: - build/reports/serenity expire_in: 30 days reports: junit: - build/test-results/test/TEST-*.xml pages: stage: report script: - mkdir -p public - cp -r build/reports/serenity/* public/ artifacts: paths: - public only: - main ``` ### Multiple Environments ```yaml stages: - test .test_template: image: maven:3.9-eclipse-temurin-17 script: - mvn clean verify -Dwebdriver.base.url=$BASE_URL artifacts: when: always paths: - target/site/serenity expire_in: 7 days test:dev: extends: .test_template variables: BASE_URL: "https://dev.example.com" only: - develop test:staging: extends: .test_template variables: BASE_URL: "https://staging.example.com" only: - staging test:production: extends: .test_template variables: BASE_URL: "https://www.example.com" only: - main when: manual ``` ### Parallel Jobs ```yaml test: stage: test parallel: matrix: - BROWSER: [chrome, firefox] SUITE: [smoke, regression] script: - mvn verify -Dwebdriver.driver=$BROWSER -Dtags=@$SUITE artifacts: when: always paths: - target/site/serenity reports: junit: - target/failsafe-reports/TEST-*.xml ``` ## Azure Pipelines For projects hosted on Azure DevOps: Create `azure-pipelines.yml`: ```yaml trigger: - main - develop pool: vmImage: 'ubuntu-latest' variables: MAVEN_CACHE_FOLDER: $(Pipeline.Workspace)/.m2/repository MAVEN_OPTS: '-Dmaven.repo.local=$(MAVEN_CACHE_FOLDER)' steps: - task: JavaToolInstaller@0 inputs: versionSpec: '17' jdkArchitectureOption: 'x64' jdkSourceOption: 'PreInstalled' - task: Cache@2 inputs: key: 'maven | "$(Agent.OS)" | **/pom.xml' path: $(MAVEN_CACHE_FOLDER) displayName: Cache Maven packages - task: Maven@3 inputs: mavenPomFile: 'pom.xml' goals: 'clean verify' options: '-B' displayName: 'Run Serenity Tests' - task: PublishTestResults@2 condition: always() inputs: testResultsFormat: 'JUnit' testResultsFiles: '**/target/failsafe-reports/TEST-*.xml' testRunTitle: 'Serenity Test Results' - task: PublishBuildArtifacts@1 condition: always() inputs: PathtoPublish: 'target/site/serenity' ArtifactName: 'serenity-report' displayName: 'Publish Serenity Report' ``` ## Docker Integration Run tests in Docker containers for consistency. ### Dockerfile for Tests ```dockerfile FROM maven:3.9-eclipse-temurin-17 # Install Chrome RUN wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | apt-key add - \ && echo "deb http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google.list \ && apt-get update \ && apt-get install -y google-chrome-stable \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY pom.xml . RUN mvn dependency:go-offline COPY src ./src CMD ["mvn", "clean", "verify"] ``` ### Docker Compose ```yaml version: '3.8' services: serenity-tests: build: . volumes: - ./target:/app/target environment: - WEBDRIVER_DRIVER=chrome - WEBDRIVER_BASE_URL=https://example.com ``` ### GitHub Actions with Docker ```yaml - name: Build and run tests in Docker run: | docker build -t serenity-tests . docker run --rm -v $PWD/target:/app/target serenity-tests ``` ## Best Practices ### 1. Use Headless Browsers Configure headless mode for CI environments: **serenity.conf:** ```hocon webdriver { driver = chrome capabilities { browserName = "chrome" "goog:chromeOptions" { args = ["--headless", "--no-sandbox", "--disable-dev-shm-usage", "--disable-gpu"] } } } ``` ### 2. Optimize Build Times **Cache dependencies:** ```yaml # GitHub Actions - uses: actions/cache@v3 with: path: ~/.m2/repository key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }} # GitLab CI cache: paths: - .m2/repository ``` **Skip unnecessary phases:** ```bash mvn verify -DskipTests # Skip test execution mvn verify -Dmaven.test.skip=true # Skip test compilation ``` ### 3. Fail Fast Configure builds to fail fast on critical errors: ```yaml strategy: fail-fast: true # Stop all jobs if one fails ``` ### 4. Tag-Based Execution Run different test suites in different pipelines: ```bash # Smoke tests on every commit mvn verify -Dcucumber.filter.tags="@smoke" # Full regression on main branch mvn verify -Dcucumber.filter.tags="@regression" ``` ### 5. Separate Test Stages ```yaml stages: - unit-tests - integration-tests - smoke-tests - regression-tests ``` ### 6. Manage Secrets Securely Never commit credentials: ```yaml # GitHub Actions env: API_KEY: ${{ secrets.API_KEY }} # GitLab CI variables: API_KEY: ${{ secrets.API_KEY }} ``` ### 7. Retry Flaky Tests ```groovy // Jenkins retry(3) { sh 'mvn verify' } ``` ```yaml # GitLab CI test: retry: max: 2 when: runner_system_failure ``` ### 8. Generate Build Badges Add status badges to your README: **GitHub Actions:** ```markdown ![Tests](https://github.com/username/repo/actions/workflows/serenity-tests.yml/badge.svg) ``` **GitLab CI:** ```markdown ![pipeline status](https://gitlab.com/username/project/badges/main/pipeline.svg) ``` ## Troubleshooting ### Issue: Tests fail only in CI **Common causes:** - Browser not installed or incompatible version - Missing display server (X11) - Insufficient memory - Network/firewall issues **Solutions:** ```yaml # Add display server for Linux before_script: - export DISPLAY=:99 - Xvfb :99 -screen 0 1920x1080x24 & # Increase memory env: MAVEN_OPTS: "-Xmx2g" ``` ### Issue: Reports not publishing **Check:** - Correct path to report directory - `if: always()` condition is set - Sufficient permissions ### Issue: Slow builds **Solutions:** - Cache dependencies - Use parallel execution - Run subset of tests (smoke tests first) - Use Docker layer caching ## Complete Example: GitHub Actions with All Features ```yaml name: Serenity BDD - Production Ready on: push: branches: [ main, develop ] pull_request: branches: [ main ] schedule: - cron: '0 2 * * *' # Daily at 2 AM workflow_dispatch: env: JAVA_VERSION: '17' MAVEN_OPTS: '-Xmx2g' jobs: smoke-tests: name: Smoke Tests runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: java-version: ${{ env.JAVA_VERSION }} distribution: 'temurin' cache: 'maven' - name: Run smoke tests run: mvn verify -Dcucumber.filter.tags="@smoke" - name: Upload smoke test report if: always() uses: actions/upload-artifact@v4 with: name: smoke-test-report path: target/site/serenity regression-tests: name: Regression Tests runs-on: ubuntu-latest needs: smoke-tests if: github.ref == 'refs/heads/main' strategy: matrix: browser: [chrome, firefox] fail-fast: false steps: - uses: actions/checkout@v4 - name: Set up JDK uses: actions/setup-java@v4 with: java-version: ${{ env.JAVA_VERSION }} distribution: 'temurin' cache: 'maven' - name: Run regression tests run: mvn verify -Dwebdriver.driver=${{ matrix.browser }} - name: Upload report if: always() uses: actions/upload-artifact@v4 with: name: report-${{ matrix.browser }} path: target/site/serenity publish-report: name: Publish Report to GitHub Pages runs-on: ubuntu-latest needs: [smoke-tests, regression-tests] if: always() && github.ref == 'refs/heads/main' permissions: contents: write pages: write steps: - uses: actions/download-artifact@v4 with: name: smoke-test-report path: reports/smoke - name: Deploy to GitHub Pages uses: peaceiris/actions-gh-pages@v3 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./reports ``` ## Next Steps - Learn about [Parallel Execution](/docs/junit5/parallel-execution) to speed up CI builds - Explore [Data-Driven Testing](/docs/tutorials/data-driven-testing) for comprehensive test coverage - Check [Serenity Configuration](/docs/reference/serenity-properties) for optimizing CI performance ## Additional Resources - [GitHub Actions Documentation](https://docs.github.com/en/actions) - [Jenkins Pipeline Documentation](https://www.jenkins.io/doc/book/pipeline/) - [GitLab CI/CD Documentation](https://docs.gitlab.com/ee/ci/) - [Serenity BDD on Docker Hub](https://hub.docker.com/r/serenity) --- # How to Do Parallel Test Execution in Cucumber with Serenity Cucumber 7 has introduced native support for parallel execution at both the feature and scenario levels. This parallel execution can provide significant performance improvements in test runs. If you want to harness these performance improvements and run your Serenity BDD features and scenarios in parallel, you will need to upgrade to JUnit 5. ## Pre-requisites - **Cucumber 7.x** - **Serenity BDD 3.9.8 or 4.0.x** - **Maven 3.x** for building the project Now, let's get started with the detailed step-by-step guide. ## Step 1: Update Serenity BDD to a Recent Version Before proceeding, make sure that you have a recent version of Serenity BDD. Versions 3.9.8 or 4.0.x are recommended for best results. You can check your current version in your `pom.xml` file or update it to the latest version. ## Step 2: Update to JUnit 5 in Your Maven Project JUnit 5 provides the foundation for running tests in parallel with Cucumber and Serenity. Here's how you can update to JUnit 5 in your `pom.xml` file: 1. Open the `pom.xml` file in your Maven project. 2. Add the following dependencies for JUnit 5 and Cucumber 7. ```xml org.junit junit-bom 5.10.0 pom import ... org.junit.platform junit-platform-launcher test org.junit.platform junit-platform-suite test org.junit.jupiter junit-jupiter-engine test io.cucumber cucumber-junit-platform-engine 7.2.3 test ``` More details on JUnit 5 and Maven can be found in the [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/#running-tests-build-maven). ## Step 3: Create a `junit-platform.properties` File Create a new file named `junit-platform.properties` in your `src/test/resources` folder. This file enables and configures parallel execution. Add the following content to the file: ```properties cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel ``` :::note Breaking Change in Serenity 5.0.0 The Cucumber plugin path changed from `io.cucumber.core.plugin.*` to `net.serenitybdd.cucumber.core.plugin.*` in Serenity 5.0.0. Make sure to use the updated path shown above. ::: These properties enable parallel execution and configure it to use a dynamic strategy, along with the Serenity reporter for parallel execution. ## Step 4: Create a `cucumber.properties` File Create a new file named `cucumber.properties` in your `src/test/resources` folder. This file will contain the Cucumber options for your Cucumber tests. Add the following content to the file: ```properties cucumber.execution.order = random cucumber.plugin=pretty,json:target/cucumber.json,timeline:target/test-results/timeline cucumber.snippet-type=camelcase ``` You can find more details on Cucumber configuration options in the [Cucumber documentation](https://cucumber.io/docs/cucumber/api/?lang=java#list-configuration-options). ## Step 5: Update Your Cucumber Runner Class You should have a single runner class for your whole test suite. Update it as follows: ```java import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("/features") public class AcceptanceTestSuite {} ``` This runner class will include all the features in the `/features` directory and run them using the Cucumber engine. ## Conclusion That's it! By following these steps, you can leverage the power of parallel execution with Cucumber and Serenity BDD. This will significantly reduce the execution time of your test suite, leading to faster feedback and more efficient development cycles. Happy testing! 🚀 --- # Three Strategies to Optimize Your Serenity BDD Reports Serenity BDD stands out for its ability to produce detailed and informative reports. These reports offer insights not just on executed tests, but also on the fulfillment of business requirements. But like all tools, getting the most out of Serenity BDD involves a few tweaks and optimizations. Here are three strategies to make your Serenity BDD reports more efficient: 1. **Stay Updated!** Ensure you're working with the latest version of Serenity BDD, ideally 4.0.12 or newer. With each update, you'll find several optimizations to enhance report performance and minimize generated files. For instance, standard report generation in version 4.0.11 is up to 10 times faster than in 3.9.8. 2. **Opt Out of Duration Reporting** By default, Serenity BDD generates in-depth reports detailing the runtime for each test. While these reports are richer in information, they consume more disk space. For substantial projects where disk space becomes an issue, consider turning off duration reporting. This can be done by setting the `serenity.report.test.durations` flag to false. As a testament to its efficacy, one project with over 11,000 tests witnessed its report size shrink from 397M to 250M, and the generation time was halved. 3. **Limit Screenshots to Failed Tests** When conducting web tests, a useful approach to conserve space is by capturing screenshots exclusively for failed tests. Achieve this by adjusting the `serenity.take.screenshots` property to FOR_FAILURES. This tweak not only accelerates the tests but also trims down the report size. Keep in mind, however, that this might slightly reduce the detail in your reporting. Harness the power of Serenity BDD more effectively by applying these strategies and enjoy a streamlined testing report experience. --- # Introduction Serenity BDD is an open source library that aims to make the idea of living documentation a reality. Serenity BDD helps you write cleaner and more maintainable automated acceptance and regression tests faster. Serenity also uses the test results to produce illustrated, narrative reports that document and describe what your application does and how it works. Serenity tells you not only what tests have been executed, but more importantly, what requirements have been tested. ![](img/intro-functional-coverage.png) One key advantage of using Serenity BDD is that you do not have to invest time in building and maintaining your own automation framework. Serenity BDD provides strong support for different types of automated acceptance testing, including: - Web testing with Selenium. - REST API testing with RestAssured. - Highly readable, maintainable and scalable automated testing with the Screenplay pattern. - BDD-style executable specifications with Cucumber Serenity not only reports on what requirements were tested, it also reports on _how_ they were tested. It includes step-by-step details of how a test played out, including optional screenshots, so your tests truly document what your application does. ![](img/intro-step-details.png) But Serenity is not just about reporting. The underlying aim of Serenity is to make it easy to quickly write well-structured, maintainable automated acceptance criteria, using your favourite BDD or conventional testing library. You can work with Behaviour-Driven-Development tools like Cucumber, or simply use JUnit (4 or 5). You can integrate with requirements stored in an external source (such as JIRA or any other test cases management tool), or just use a simple directory-based approach to organise your requirements. ![](img/intro-scenario.png) Serenity also provides a host of built-in features that make coding your test automation faster and more reliable. Testers working with Serenity typically report automating tests with less than half the code they would need for a conventional Selenium test. :::info Up until November 2014, Serenity went by the name of Thucydides. Thucydides was an Ancient Greek historian who wrote about the Peloponnesian War in the late 5th Century BCE. He was the first historian to write about events that he actually participated in, and in many cases, he wrote down the events that he directly witnessed. In a similar way Serenity BDD not only reports on test outcomes, but also records and documents how each scenario plays out, giving a particularly rich form of test reporting and living documentation. ::: :::tip Using an AI coding assistant Serenity has several equally valid ways of writing tests, and AI coding assistants often guess a package name or mix two of them. See [Using Serenity BDD with AI Coding Assistants](/docs/ai/ai_using_assistants) for pages with verified packages and idioms, `AGENTS.md` files for each style, an agent skill and `llms.txt`. ::: ![](img/intro-hoplites.jpeg) --- # Getting Started ## Creating A New Project With The Serenity BDD Templates The quickest way to get started with a Serenity BDD project is to use one of the starter projects. These are template projects available on Github, that contain a basic Serenity BDD skeleton project and a single test implementation. These template projects are kept up-to-date with the most recent stable version of Serenity BDD. The main available starter projects are the following: | Template | Description | Get Started | | -------- | ----------- | ------------| | [Serenity BDD with JUnit](https://github.com/serenity-bdd/serenity-junit-starter) | A simple project structure using Serenity BDD, JUnit 5 and an Action Classes/Page Objects architecture. | **[Use This Template](https://github.com/serenity-bdd/serenity-junit-starter/generate)** | | [Serenity BDD with JUnit and Screenplay](https://github.com/serenity-bdd/serenity-junit-screenplay-starter) | A project built around the Screenplay Pattern using Serenity BDD, JUnit 5 and Serenity Screenplay | **[Use This Template](https://github.com/serenity-bdd/serenity-junit-screenplay-starter/generate)** | | [Serenity BDD with Cucumber and Screenplay](https://github.com/serenity-bdd/serenity-cucumber-starter) | A BDD-style test suite using Cucumber and Serenity Screenplay | **[Use This Template](https://github.com/serenity-bdd/serenity-cucumber-starter/generate)** | To create a new project, go to the project template you want to use, and click on the "Use this template" button. This will allow you to create a new project in your own Github repository. ![](img/use-this-template.png) If you are working on an internal project, you can also download the template project by clicking on the "Code" button. --- # Integrating Serenity BDD into a Maven project Maven is the recommended build tool for Serenity BDD. You can add Serenity BDD to an existing Maven project by adding the corresponding dependencies to your `pom.xml` file. ## Using the Serenity BDD BOM (Recommended) The simplest way to manage Serenity BDD dependencies is to use the **Bill of Materials (BOM)**. The BOM ensures that all Serenity modules use the same version, so you don't need to specify version numbers on each individual dependency. Add the BOM to your `` section: ```xml net.serenity-bdd serenity-bom 5.4.1 pom import ``` Then declare your Serenity dependencies **without version tags**: ```xml net.serenity-bdd serenity-core test net.serenity-bdd serenity-junit5 test net.serenity-bdd serenity-screenplay-webdriver test net.serenity-bdd serenity-ensure test ``` :::tip Upgrading When a new version of Serenity BDD is released, you only need to update the single version number in the BOM import. All module versions will be updated automatically. ::: ## Managing Versions Manually If you prefer not to use the BOM, you can manage versions manually by defining them in your properties section: ```xml 5.4.1 6.0.3 7.34.2 ``` ## Core Dependencies All Serenity BDD projects need the following core dependency: ``` net.serenity-bdd serenity-core ${serenity.version} test ``` You will also need a test runner, which will usually be either JUnit 5 (recommended) or Cucumber. Note that JUnit 4 is deprecated as of Serenity 5.0.0. ## Serenity JUnit 5 Dependencies (Recommended) To use JUnit 5 you will need the following dependency: ``` net.serenity-bdd serenity-junit5 ${serenity.version} test ``` You will also need the JUnit 5 dependencies, e.g. ``` org.junit.jupiter junit-jupiter-api ${junit5.version} test org.junit.jupiter junit-jupiter-engine ${junit5.version} test ``` ## Serenity JUnit 4 Dependencies (Deprecated) :::warning JUnit 4 Deprecated JUnit 4 support is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 (see above). ::: If you're still using JUnit 4, you will need the following dependency: ``` net.serenity-bdd serenity-junit ${serenity.version} test ``` ## Serenity Cucumber Dependencies For Cucumber you will need the following dependency: ``` net.serenity-bdd serenity-cucumber ${serenity.version} test ``` Since Serenity relies on a specific version of the Cucumber APIs, we recommend using the same version of the Cucumber libraries. Cucumber requires a test runner, preferably JUnit 5 (JUnit 4 is deprecated). ## Cucumber with JUnit 5 Dependencies (Recommended) To use JUnit 5 you will need the following dependency: ``` io.cucumber cucumber-junit-platform-engine ${cucumber.version} test org.junit.platform junit-platform-suite ${junit-platform.version} test ``` See cucumber-junit-platform-engine documentation on using junit-platform-suite to pilot cucumber: https://github.com/cucumber/cucumber-jvm/tree/main/junit-platform-engine#suites-with-different-configurations, and https://github.com/serenity-bdd/serenity-cucumber-starter for a running example. ## Cucumber with JUnit 4 Dependencies (Deprecated) :::warning JUnit 4 Deprecated JUnit 4 Cucumber runners are deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 (see above). ::: If you're still using JUnit 4 with Cucumber, you will need the following dependency: ``` io.cucumber cucumber-junit ${cucumber.version} test org.junit.platform junit-vintage-engine ${junit5.version} test ``` ## Screenplay If you are using the Screenplay pattern, you will also need the Screenplay dependencies: ``` net.serenity-bdd serenity-screenplay ${serenity.version} test net.serenity-bdd serenity-rest-assured ${serenity.version} net.serenity-bdd serenity-ensure ${serenity.version} test net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} test ``` ## Running Tests with Maven Failsafe Serenity tests are integration tests and should be run using the Maven Failsafe plugin. The basic configuration is straightforward: ```xml org.apache.maven.plugins maven-failsafe-plugin 3.5.2 **/*Test.java integration-test verify ``` ### Running JUnit 5 and Cucumber Tests Together If your project contains both JUnit 5 tests and Cucumber tests, you may encounter a problem where only Cucumber tests are discovered and run. This happens because when Cucumber uses the `cucumber.features` property (either via `@ConfigurationParameter` or `junit-platform.properties`), it causes other JUnit Platform discovery selectors to be ignored. You'll see a warning like this: ``` WARNING: TestEngine with ID 'cucumber' encountered a non-critical issue during test discovery: Discovering tests using the cucumber.features property. Other discovery selectors are ignored! ``` **The solution** is to configure separate Failsafe executions for JUnit 5 and Cucumber tests: ```xml org.apache.maven.plugins maven-failsafe-plugin 3.5.2 junit-tests integration-test **/*Test.java **/CucumberTestSuite.java cucumber-tests integration-test **/CucumberTestSuite.java verify verify ``` This configuration: - Runs JUnit Jupiter tests first (all `*Test.java` files except the Cucumber suite) - Runs Cucumber tests separately via the `CucumberTestSuite` class - Runs the verify goal once at the end to check for failures :::tip Naming Convention Adjust the `` and `` patterns to match your test class naming conventions. For example, if your Cucumber suite is named `CucumberIT.java`, update the patterns accordingly. ::: ## The Serenity Maven Plugin If you want to generate the Serenity reports whenever you run `mvn verify`, you can use the `serenity-maven-plugin` to do that: ``` net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} ${tags} serenity-reports post-integration-test aggregate check ``` By default, the `aggregate` goal will NOT fail the build if there are test failures - it will simply generate the reports. This way you can aggregate multiple report sets and then check for failures at the end using `mvn serenity:check`. :::note When tests fail, `mvn verify` does not build the report The `aggregate` goal is bound to the `post-integration-test` phase, which Maven never reaches when the tests fail. The per-test JSON files are still written to `target/site/serenity`, but there is no `index.html`. Either run the report goal after the failed build: ``` mvn net.serenity-bdd.maven.plugins:serenity-maven-plugin:aggregate ``` or let the build continue past test failures with `-Dmaven.test.failure.ignore=true` (the exit code is then 0 even when tests fail, so check the results another way on a CI server). ::: :::note Old results are counted Serenity reports every outcome file it finds in `target/site/serenity`, including files left by earlier runs. Use `mvn clean verify`, or delete that directory, so the report only covers the latest run. ::: ## Checking Gherkin feature files Some errors in Gherkin feature files can cause the Serenity reports to behave unpredictably. For this reason, the following rules should be followed when working with Cucumber and Serenity: - Scenario names should be unique within a feature file - Scenario, Rule and Feature names should not be blank - Feature names should be unique wherever possible. In particular features with the same name, inside directories with identical names, will not appear correctly in the Serenity reports. You can check these rules before you run the full test by calling the `check-gherkin` goal, e.g. ``` mvn serenity:check-gherkin ``` You can ensure that your feature files are correctly configured before kicking off your tests by binding the `check-gherkin` goal to the `process-test-resources` lifecycle phase, as shown here: ``` net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} ${tags} check-feature-files process-test-resources check-gherkin serenity-reports post-integration-test aggregate ``` ## Complete Example: JUnit 5 with BOM Here is a complete `pom.xml` for a Serenity BDD project using JUnit 5, the Screenplay pattern, and the BOM: ```xml 4.0.0 com.example my-serenity-project 1.0-SNAPSHOT 17 17 5.4.1 net.serenity-bdd serenity-bom ${serenity.version} pom import net.serenity-bdd serenity-core test net.serenity-bdd serenity-junit5 test net.serenity-bdd serenity-screenplay test net.serenity-bdd serenity-screenplay-webdriver test net.serenity-bdd serenity-ensure test org.apache.maven.plugins maven-failsafe-plugin 3.5.2 **/*Test.java integration-test verify net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} serenity-reports post-integration-test aggregate ``` ## Complete Example: Cucumber with BOM Here is a complete `pom.xml` for a Serenity BDD project using Cucumber, JUnit 5, and the BOM: ```xml 4.0.0 com.example my-serenity-cucumber-project 1.0-SNAPSHOT 17 17 5.4.1 7.34.2 net.serenity-bdd serenity-bom ${serenity.version} pom import net.serenity-bdd serenity-core test net.serenity-bdd serenity-cucumber test net.serenity-bdd serenity-screenplay test net.serenity-bdd serenity-screenplay-webdriver test net.serenity-bdd serenity-ensure test io.cucumber cucumber-junit-platform-engine ${cucumber.version} test org.junit.platform junit-platform-suite test org.apache.maven.plugins maven-failsafe-plugin 3.5.2 **/CucumberTestSuite.java integration-test verify net.serenity-bdd.maven.plugins serenity-maven-plugin ${serenity.version} serenity-reports post-integration-test aggregate ``` ## Available BOM Modules The `serenity-bom` includes version-managed entries for all published Serenity BDD modules: | Category | Modules | | -------- | ------- | | **Core** | `serenity-core`, `serenity-model`, `serenity-report-resources` | | **Test Runners** | `serenity-junit5`, `serenity-junit` (deprecated), `serenity-cucumber` | | **Screenplay** | `serenity-screenplay`, `serenity-screenplay-webdriver`, `serenity-screenplay-rest`, `serenity-screenplay-playwright` | | **Assertions** | `serenity-ensure`, `serenity-assertions` | | **REST** | `serenity-rest-assured` | | **Reporting** | `serenity-reports`, `serenity-single-page-report`, `serenity-json-summary-report`, `serenity-navigator-report` | | **Integrations** | `serenity-spring`, `serenity-playwright` | | **Cloud Providers** | `serenity-browserstack`, `serenity-saucelabs`, `serenity-lambdatest`, `serenity-selenoid`, `serenity-bitbar`, `serenity-zalenium`, `serenity-crossbrowsertesting` | | **Tools** | `serenity-browsermob-plugin`, `serenity-shutterbug`, `serenity-cli`, `serenity-ant-task` | | **JIRA** | `serenity-jira-plugin`, `serenity-jira-requirements-provider` | --- # Serenity Step Libraries In Serenity, tests are broken down into reusable steps. An important principle behind Serenity is the idea that it is easier to maintain a test that uses several layers of abstraction to hide the complexity behind different parts of a test. In an automated acceptance test, test steps represent the level of abstraction between the code that interacts with your application (for example, Page Component Objects in an automated web test, which model parts of the user interface, or API endpoints for web services you need to interact with) and higher-level stories (sequences of more business-focused actions that illustrate how a given user story has been implemented). If your automated test is not UI-oriented (for example, if it calls a web service), steps orchestrate other more technical components such as REST clients. Steps can contain other steps, and are included in the Serenity reports. Whenever a UI step is executed, a screenshot is stored and displayed in the report. ## Breaking down tests into steps Suppose we are testing a Frequent Flyer programme, and need to illustrate the following business rules: - Members should start with Bronze status - Members should earn Silver status after flying 10,000 km. Each of these can be broken down further into business tasks and verifications. For example, the first rule could be broken into two steps: - Create a new Frequent Flyer member - Check that the member has a status of Bronze And the second can be broken into three steps: - Create a new Frequent Flyer member - Make the member fly 10000 km - Check that the member has a status of Silver We could express these tests using Serenity in JUnit 5 as follows: ```java package flyer; import net.serenitybdd.junit5.SerenityJUnit5Extension; import net.serenitybdd.annotations.Steps; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import flyer.steps.TravellerEarningStatusPoints; import static flyer.Status.Bronze; import static flyer.Status.Silver; @ExtendWith(SerenityJUnit5Extension.class) class WhenEarningFrequentFlyerPoints { @Steps TravellerEarningStatusPoints tracy; @Test void members_should_start_with_Bronze_status() { // GIVEN tracy.joins_the_frequent_flyer_program(); // THEN tracy.should_have_a_status_of(Bronze); } @Test void earn_silver_after_10000_kilometers() { // GIVEN tracy.joins_the_frequent_flyer_program(); // WHEN tracy.flies(10000); // THEN tracy.should_have_a_status_of(Silver); } } ``` :::note JUnit 4 Deprecated If you're still using JUnit 4 with `@RunWith(SerenityRunner.class)`, note that JUnit 4 support is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 using `@ExtendWith(SerenityJUnit5Extension.class)` as shown above. ::: Notice how the second test reuses step methods used in the first to perform a slightly different test. This is a typical example of the way we reuse steps in similar tests, in order to avoid duplicated code and make the code easier to maintain. ## Implementing Simple Step Libraries The `TravellerEarningStatusPoints` class is what we call a step library. We use the `@Steps` class as shown above to indicate a step library in our test code: this annotation tells Serenity to instantiate and instrument this field, so that methods you call in this library also appear in the test reports. Step libraries contain the business tasks or actions that a user performs during a test. There are many ways to organise your step libraries, but a convenient way is to group methods in slices of business behaviour for a given type of user. In this case a traveller who is earning status points. Notice that it was not necessary to explicitly instantiate the Steps class `TravellerEarningStatusPoints`. When you annotated a member variable of this class with the `@Steps` annotation, Serenity BDD will automatically instantiate it for you. You should never create instances of step libraries using the `new` keyword, as Serenity will not be able to instrument the step library correctly, and the methods called will not appear in the reports. For example, suppose we want to test a simple calculator UI. In this class, we use the `CalculatorSteps` step library, which we annotate with the `@Steps` annotation: ```java @ExtendWith(SerenityJUnit5Extension.class) class WhenDoingSums { @Steps CalculatorSteps calulate; @Nested class ABasicCalculator { @BeforeEach void openTheCalculator() { calulate.openTheCalculatorApp(); } @Test void shouldCalculateAdditions() { int result = calulate.theAnswerTo("1","+","2"); assertThat(result).isEqualTo(3); } @Test void shouldCalculateSubtractions() { int result = calulate.theAnswerTo("7","-","3"); assertThat(result).isEqualTo(4); } @Test void shouldCalculateMultiplications() { int result = calulate.theAnswerTo("3","*","2"); assertThat(result).isEqualTo(6); } @Test void shouldCalculateDivisions() { int result = calulate.theAnswerTo("10","/","2"); assertThat(result).isEqualTo(5); } } } ``` The `CalculatorSteps` class defines the steps a user needs to perform - in this case, perform a calculation on the calculator app: ```java public class CalculatorSteps { @Step("Calculate the answer to {0} {1} {2}") public int theAnswerTo(String a, String operator, String b) { // Interact with the the calculator to perform the calculation } } ``` Step methods are annotated with the `@Step` annotation, which tells Serenity to include this method in the Serenity test reports. The value we include in the `@Step` annotation is the text that will appear in the Serenity reports when this step is executed. The numbers in curly brackets (`{0}`,`{1}` and `{2}`) represent placeholders the method parameters, if we want them to appear in the reports. When we run this test case, Serenity will document the steps that get executed during each test, producing a living documentation of the test execution: ![](img/calculator-steps.png) ### UI Interaction Classes Suppose our calculator test needs to interact with a web UI, such as the one on https://juliemr.github.io/protractor-demo/ shown here: ![](img/calculator-app.png) We can access the powerful Serenity BDD Selenium integration in our step libraries by extending the `net.serenitybdd.core.steps.UIInteractions` class. A very simple implementation might look like this: ```java public class CalculatorSteps extends UIInteractions { @Step public void openTheCalculatorApp() { openUrl("https://juliemr.github.io/protractor-demo/"); } @Step("Calculate the answer to {0} {1} {2}") public int theAnswerTo(String firstValue, String operator, String secondValue) { $("[ng-model=first]").sendKeys(firstValue); $("[ng-model=operator]").selectByVisibleText(operator); $("[ng-model=second]").sendKeys(secondValue); $("#gobutton").click(); waitForAngularRequestsToFinish(); return Integer.parseInt($("css:h2").getText()); } } ``` :::caution Do not declare `@Managed WebDriver` in a step library `@Managed` is only filled in on the test class. In a step library the field stays `null`, and the first use fails with a `NullPointerException: Cannot invoke "org.openqa.selenium.WebDriver.get(String)" because "this.driver" is null`, which does not mention `@Managed`. Extend `UIInteractions` and use `getDriver()`, or use a page object, which gets the driver by itself. In releases after 5.4.1, Serenity also logs a warning when it finds a `@Managed` field in a step library. ::: A more readable, refactored implementation might look like this: ```java public class CalculatorSteps extends UIInteractions { private static final By FIRST_VALUE_FIELD = By.cssSelector("[ng-model=first]"); private static final By SECOND_VALUE_FIELD = By.cssSelector("[ng-model=second]"); private static final By OPERATOR_DROPDOWN = By.cssSelector("[ng-model=operator]"); private static final By GO_BUTTON = By.id("gobutton"); private static final By RESULT_FIELD = By.tagName("h2"); private static final Pattern A_VALID_NUMBER = Pattern.compile("-?\\d\\.?d*"); @Step public void openTheCalculatorApp() { openUrl("https://juliemr.github.io/protractor-demo/"); } @Step("Calculate the answer to {0} {1} {2}") public int theAnswerTo(String firstValue, String operator, String secondValue) { $(FIRST_VALUE_FIELD).sendKeys(firstValue); $(OPERATOR_DROPDOWN).selectByVisibleText(operator); $(SECOND_VALUE_FIELD).sendKeys(secondValue); $(GO_BUTTON).click(); waitFor(ExpectedConditions.textMatches(RESULT_FIELD,A_VALID_NUMBER)); return Integer.parseInt($(RESULT_FIELD).getText()); } } ``` The tests will now interact with the user interface, report on both the steps executed, and (if configured to do so) record screenshots for each step: ![](img/calculator-step-screenshots.png) ### Persona Step Libraries Another approach to modeling step libraries revolves around actors and roles. Let's return to our original example: Tracy, the traveller, who earns points when she travels. ```java public class TravellerEarningStatusPoints { private String actor; private FrequentFlyer frequentFlyer; @Step("#actor joins the frequent flyer program") public void joins_the_frequent_flyer_program() { frequentFlyer = FrequentFlyer.withInitialBalanceOf(0); } @Step("#actor flies {0} km") public void flies(int distance) { frequentFlyer.recordFlightDistanceInKilometers(distance); } @Step("#actor should have a status of {0}") public void should_have_a_status_of(Status expectedStatus) { assertThat(frequentFlyer.getStatus()).isEqualTo(expectedStatus); } @Step("#actor transfers {0} points to {1}") public void transfers_points(int points, TravellerEarningStatusPoints otherFrequentFlier) { // Left as an exercise } @Override public String toString() { return actor; } @Step("#actor should have {0} points") public void should_have_points(int expectedPoints) { // Left as an exercise } } ``` This approach uses the concept of _persona_ step libraries, where a step library represents the actions of a specific user performing a specific task (Tracy the traveller who is earning points.) In this case, Serenity will automatically instantiate the _actor_ field with the name of the step library variable (`tracy`), allowing it to appear in the test reports to make them more readable: ![](img/persona-steps.png) ## Shared Instances of Step Libraries There are some cases where we want to reuse the same step library instance in different places across a test. For example, suppose we have a step library that interacts with a backend API, and that maintains some internal state and caching to improve performance. We might want to reuse a single instance of this step library, rather than having a separate instance for each variable. We can do this by declaring the step library to be shared, like this: ```java @Steps(shared = true) CustomerAPIStepLibrary customerAPI; ``` Now, any other step libraries of type `CustomerAPIStepLibrary`, that have the shared attribute set to true will refer to the same instance. In older versions of Serenity, sharing instances was the default behaviour, and you used the uniqueInstance attribute to indicate that a step library should not be shared. If you need to force this behaviour for legacy test suites, set the step.creation.strategy property to legacy in your serenity.properties file: ```javascript step.creation.strategy = legacy ``` --- # Interacting with Web Pages Serenity integrates smoothly with Selenium WebDriver and manages details such as setting up the driver and maintaining the driver instance. It also provides a number of enhancements to standard Selenium. ## Page Objects If you are working with WebDriver web tests, you will be familiar with the concept of Page Objects. Page Objects are a way of isolating the implementation details of a web page inside a class, exposing only business-focused methods related to that page. They are an excellent way of making your web tests more maintainable. In Serenity, page objects are just ordinary classes that extend the `PageObject` class. Serenity automatically injects a WebDriver instance into the page object that you can access via the `getDriver()` method, although you rarely need to use the WebDriver directly. The Serenity `PageObject` class provides a number of convenient methods that make accessing and manipulating web elements much easier than with conventional WebDriver test scripts. Here is a simple example: ```java ... import net.serenitybdd.core.pages.WebElementFacade; import net.thucydides.core.pages.PageObject; ... @DefaultUrl("http://localhost:9000/somepage") public class FindAJobPage extends PageObject { WebElementFacade keywords; WebElementFacade searchButton; public void look_for_jobs_with_keywords(String values) { typeInto(keywords, values); searchButton.click(); } public List getJobTabs() { return findAll("//div[@id='tabs']//a").stream() .map(WebElementFacade::getText) .collect(Collectors.toList()); } } ``` The `typeInto` method is a shorthand that simply clears a field and enters the specified text. If you prefer a more fluent-API style, you can also do something like this: ```java @DefaultUrl("http://localhost:9000/somepage") public class FindAJobPage extends PageObject { WebElementFacade keywordsField; WebElementFacade searchButton; public FindAJobPage(WebDriver driver) { super(driver); } public void look_for_jobs_with_keywords(String values) { enter(values).into(keywordsField); searchButton.click(); } public List getJobTabs() { return findAll("//div[@id='tabs']//a").stream() .map(WebElementFacade::getText) .collect(Collectors.toList()); } } ``` You can use an even more fluent style of expressing the implementation steps by using methods like `find`, `findBy` and `then`. For example, you can use webdriver `By` finders with element name, id, css selector or xpath selector as follows: ```java find(By.name("demo")).then(By.name("specialField")).getValue(); find(By.cssSelector(".foo")).getValue(); find(By.xpath("//th")).getValue(); ``` The `findBy` method lets you pass the css or xpath selector directly to WebDriver. For example, ```java findBy("#demo").then("#specialField").getValue(); //css selectors findBy("//div[@id='dataTable']").getValue(); //xpath selector ``` ## Using pages in a step library When you need to use a page object in one of your steps, you just need to declare a variable of type PageObject in your step library, e.g. ```java FindAJobPage page; ``` If you want to make sure you are on the right page, you can use the `currentPageAt()` method. This will check the page class for any `@At` annotations present in the PageObject class and, if present, check that the current URL corresponds to the URL pattern specified in the annotation. For example, when you invoke it using `currentPageAt()+, the following Page Object will check that the current URL is precisely `http://www.apache.org`. ```java @At("http://www.apache.org") public class ApacheHomePage extends PageObject { ... } ``` The `@At` annotation also supports wildcards and regular expressions. The following page object will match any Apache sub-domain: ```java @At("http://.*.apache.org") public class AnyApachePage extends PageObject { ... } ``` More generally, however, you are more interested in what comes after the host name. You can use the special `#HOST` token to match any server name. So the following Page Object will match both `http://localhost:8080/app/action/login.form` and `http://staging.acme.com/app/action/login.form`. It will also ignore parameters, so `http://staging.acme.com/app/action/login.form?username=toto&password=oz` will work fine too. ```java @At(urls={"#HOST/app/action/login.form"}) public class LoginPage extends PageObject { ... } ``` ## Opening the page A page object is usually designed to work with a particular web page. When the `open()` method is invoked, the browser will be opened to the default URL for the page. The `@DefaultUrl` annotation indicates the URL that this test should use when run in isolation (e.g. from within your IDE). Generally, however, the host part of the default URL will be overridden by the `webdriver.base.url` property, as this allows you to set the base URL across the board for all of your tests, and so makes it easier to run your tests on different environments by simply changing this property value. For example, in the test class above, setting the `webdriver.base.url` to 'https://staging.mycompany.com' would result in the page being opened at the URL of 'https://staging.mycompany.com/somepage'. You can also define named URLs that can be used to open the web page, optionally with parameters. For example, in the following code, we define a URL called 'open.issue', that accepts a single parameter: ```java @DefaultUrl("http://jira.mycompany.org") @NamedUrls( { @NamedUrl(name = "open.issue", url = "http://jira.mycompany.org/issues/{1}") } ) public class JiraIssuePage extends PageObject { ... } ``` You could then open this page to the http://jira.mycompany.org/issues/ISSUE-1 URL as shown here: ```java page.open("open.issue", withParameters("ISSUE-1")); ``` You could also dispense entirely with the base URL in the named URL definition, and rely on the default values: ```java @DefaultUrl("http://jira.mycompany.org") @NamedUrls( { @NamedUrl(name = "open.issue", url = "/issues/{1}") } ) public class JiraIssuePage extends PageObject { ... } ``` And naturally you can define more than one definition: ```java @NamedUrls( { @NamedUrl(name = "open.issue", url = "/issues/{1}"), @NamedUrl(name = "close.issue", url = "/issues/close/{1}") } ) ``` You should never try to implement the `open()` method yourself. In fact, it is final. If you need your page to do something upon loading, such as waiting for a dynamic element to appear, you can use the @WhenPageOpens annotation. Methods in the PageObject with this annotation will be invoked (in an unspecified order) after the URL has been opened. In this example, the `open()` method will not return until the `dataSection` web element is visible: ```java @DefaultUrl("http://localhost:8080/client/list") public class ClientList extends PageObject { @FindBy(id="data-section"); WebElementFacade dataSection; ... @WhenPageOpens public void waitUntilTitleAppears() { element(dataSection).waitUntilVisible(); } } ``` ## Working with web elements ### Checking whether elements are visible The `WebElementFacade` class contains convenient fluent API for dealing with web elements, providing some commonly-used extra features that are not provided out-of-the-box by the WebDriver API. `WebElementFacades` are largely interchangeable with WebElements: you just declare a variable of type `WebElementFacade` instead of type `WebElement`. For example, you can check that an element is visible as shown here: ```java public class FindAJobPage extends PageObject { WebElementFacade searchButton; public boolean searchButtonIsVisible() { return searchButton.isVisible(); } ... } ``` If the button is not present on the screen, the test will wait for a short period in case it appears due to some Ajax magic. If you don't want the test to do this, you can use the faster version: ```java public boolean searchButtonIsVisibleNow() { return searchButton.isCurrentlyVisible(); } ``` You can turn this into an assert by using the `shouldBeVisible()` method instead: ```java public void checkThatSearchButtonIsVisible() { searchButton.shouldBeVisible(); } ``` This method will throw an assertion error if the search button is not visible to the end user. ### Checking whether elements are enabled You can also check whether an element is enabled or not: ```java searchButton.isEnabled() searchButton.shouldBeEnabled() ``` There are also equivalent negative methods: ```java searchButton.shouldNotBeVisible(); searchButton.shouldNotBeCurrentlyVisible(); searchButton.shouldNotBeEnabled() ``` You can also check for elements that are present on the page but not visible, e.g: ```java searchButton.isPresent(); searchButton.isNotPresent(); searchButton.shouldBePresent(); searchButton.shouldNotBePresent(); ``` ### Manipulating select lists There are also helper methods available for drop-down lists. Suppose you have the following dropdown on your page: ```xml ``` You could write a page object to manipulate this dropdown as shown here: ```java public class FindAJobPage extends PageObject { @FindBy(id="color") WebElementFacade colorDropdown; public selectDropdownValues() { colorDropdown.selectByVisibleText("Blue"); assertThat(colorDropdown.getSelectedVisibleTextValue(), is("Blue")); colorDropdown.selectByValue("blue"); assertThat(colorDropdown.getSelectedValue(), is("blue")); colorDropdown.selectByIndex(2); assertThat(colorDropdown.getSelectedValue(), is("green")); } ... } ``` ### Determining focus You can determine whether a given field has the focus as follows: ```java firstName.hasFocus() ``` You can also wait for elements to appear, disappear, or become enabled or disabled: ```java button.waitUntilEnabled() button.waitUntilDisabled() ``` or ```java field.waitUntilVisible() button.waitUntilNotVisible() ``` ### Using direct XPath and CSS selectors Another way to access a web element is to use an XPath or CSS expression. You can use the `$()` method with an XPath expression to do this more simply. For example, imagine your web application needs to click on a list item containing a given post code. One way would be as shown here: ```java WebElement selectedSuburb = getDriver().findElement(By.xpath("//li/a[contains(.,'" ` postcode ` "')]")); selectedSuburb.click(); ``` However, a simpler option would be to do this: ```java $("//li/a[contains(.,'" ` postcode ` "')]").click(); ``` ## Working with Asynchronous Pages Asynchronous pages are those whose fields or data is not all displayed when the page is loaded. Sometimes, you need to wait for certain elements to appear, or to disappear, before being able to proceed with your tests. Serenity provides some handy methods in the PageObject base class to help with these scenarios. They are primarily designed to be used as part of your business methods in your page objects, though in the examples we will show them used as external calls on a PageObject instance for clarity. ### Checking whether an element is visible In WebDriver terms, there is a distinction between when an element is present on the screen (i.e. in the HTML source code), and when it is rendered (i.e. visible to the user). You may also need to check whether an element is visible on the screen. You can do this in two ways. Your first option is to use the isElementVisible method, which returns a boolean value based on whether the element is rendered (visible to the user) or not: ```java isElementVisible(By.xpath("//h2[.='A visible title']")) ``` Your second option is to actively assert that the element should be visible: ```java shouldBeVisible(By.xpath("//h2[.='An invisible title']")); ``` If the element does not appear immediately, you can wait for it to appear: ```java waitForRenderedElements(By.xpath("//h2[.='A title that is not immediately visible']")); ``` An alternative to the above syntax is to use the more fluid `waitFor` method which takes a css or xpath selector as argument: ```java waitFor("#popup"); //css selector waitFor("//h2[.='A title that is not immediately visible']"); //xpath selector ``` If you just want to check if the element is present though not necessarily visible, you can use `waitForRenderedElementsToBePresent` : ```java waitForRenderedElementsToBePresent(By.xpath("//h2[.='A title that is not immediately visible']")); ``` or its more expressive flavour, `waitForPresenceOf` which takes a css or xpath selector as argument. ```java waitForPresenceOf("#popup"); //css waitForPresenceOf("//h2[.='A title that is not immediately visible']"); //xpath ``` You can also wait for an element to disappear by using `waitForRenderedElementsToDisappear` or `waitForAbsenceOf` : ```java waitForRenderedElementsToDisappear(By.xpath("//h2[.='A title that will soon disappear']")); waitForAbsenceOf("#popup"); waitForAbsenceOf("//h2[.='A title that will soon disappear']"); ``` For simplicity, you can also use the `waitForTextToAppear` and `waitForTextToDisappear` methods: ```java waitForTextToDisappear("A visible bit of text"); ``` If several possible texts may appear, you can use `waitForAnyTextToAppear` or `waitForAllTextToAppear+: ```java waitForAnyTextToAppear("this might appear","or this", "or even this"); ``` If you need to wait for one of several possible elements to appear, you can also use the `waitForAnyRenderedElementOf` method: ```java waitForAnyRenderedElementOf(By.id("color"), By.id("taste"), By.id("sound")); ``` ### Working with Shadow DOM elements Selenium 4 introduced support for Shadow DOM elements, and Serenity BDD adds enhanced support for these elements. Suppose we have the following HTML structure containing shadow DOM elements. ```html
    
#shadow-root
#shadow-root
``` To find the first input element inside a single-level shadow DOM, you need to provide the locator for the nested element and the locator for the shadow host element: ```java ByShadow.cssSelector("#shadowedInput","#shadow-host") ``` To find the input element inside the nested shadow DOM, you need to provide the locator for the shadowed element, as well as the list of parent shadow dom locators, from top to bottom: ```java ByShadow.cssSelector("#nestedShadowedInput","#shadow-host", "#nested-shadow-host") ``` ## Working with timeouts Modern AJAX-based web applications add a great deal of complexity to web testing. The basic problem is, when you access a web element on a page, it may not be available yet. So you need to wait a bit. Indeed, many tests contain hard-coded pauses scattered through the code to cater for this sort of thing. But hard-coded waits are evil. They slow down your test suite, and cause them to fail randomly if they are not long enough. Rather, you need to wait for a particular state or event. Selenium provides great support for this, and Serenity builds on this support to make it easier to use. ### Implicit Waits The first way you can manage how WebDriver handles tardy fields is to use the `webdriver.timeouts.implicitlywait` property. This determines how long, in milliseconds, WebDriver will wait if an element it tries to access is not present on the page. To quote the WebDriver documentation: “An implicit wait is to tell WebDriver to poll the DOM for a certain amount of time when trying to find an element or elements if they are not immediately available.” The default value in Serenity for this property is currently 2 seconds. This is different from standard WebDriver, where the default is zero. Let’s look at an example. Suppose we have a PageObject with a field defined like this: ```java @FindBy(id="slow-loader") public WebElementFacade slowLoadingField; ``` This field takes a little while to load, so won’t be ready immediately on the page. Now suppose we set the `webdriver.timeouts.implicitlywait` value to 5000, and that our test uses the slowLoadingField: ```java boolean loadingFinished = slowLoadingField.isDisplayed() ``` When we access this field, two things can happen. If the field takes less than 5 seconds to load, all will be good. But if it takes more than 5 seconds, a NoSuchElementException (or something similar) will be thrown. That this timeout also applies for lists. Suppose we have defined a field like this, which takes some time to dynamically load: ```java @FindBy(css="#elements option") public List elementItems; ``` Now suppose we count the values of the element like this: ```java int itemCount = elementItems.size() ``` The number of items returned will depend on the implicit wait value. If we set the `webdriver.timeouts.implicitlywait` value to a very small value, WebDriver may only load some of the values. But if we give the list enough time to load completely, we will get the full list. The implicit wait value is set globally for each WebDriver instance, but you can override the value yourself. The simplest way to do this from within a Serenity PageObject is to use the setImplicitTimeout() method: ```java setImplicitTimeout(5, SECONDS) ``` But remember this is a global configuration, so will also affect other page objects. So once you are done, you should always reset the implicit timeout to its previous value. Serenity gives you a handy method to do this: ```java resetImplicitTimeout() ``` See http://docs.seleniumhq.org/docs/04_webdriver_advanced.jsp#implicit-waits[Selenium Documentation] for more details on how the WebDriver implicit waits work. ### Using custom locator factories Internally, Selenium uses the concept of Locator Factories Normally, Serenity uses `SmartElementLocatorFactory`, an extension of the WebDriver `AjaxElementLocatorFactory`, when instantiating page objects. Among other things, this helps ensure that web elements are available and usable before they are used, allows field-by-field timeouts, and avoids long unnecessary waits on web elements after a step has failed. The `SmartElementLocatorFactory` uses the default implicit wait, or the `timeoutInSeconds` attribute of the `@FindBy` annotation if this value has been specified (see below), or the default implicit wait value specified by the `webdriver.timeouts.implicitlywait` property. In rare cases, you may need to customise this behaviour. To do this, you can use the `serenity.locator.factory` property to use one of the following alternative locator factories: - `AjaxElementLocatorFactory`: A WebDriver locator factory more suitable for Ajax applications. According to the WebDriver docs, this locator factory will return _an element locator that will wait for the specified number of seconds for an element to appear, rather than failing instantly if it's not present. This works by polling the UI on a regular basis. The element returned will be present on the DOM, but may not actually be visible._ - `DefaultElementLocatorFactory`: the default WebDriver locator factory If you use the `AjaxElementLocatorFactory`, you can use the `webdriver.timeouts.implicitlywait` parameter is to specify the number of seconds to wait. If no value is specified, the default wait will be 5 seconds. ### Explicit Timeouts You can also programatically wait until an element is in a particular state. This is more flexible and useful when you need to wait for extra time in a specific situation. For example, we could wait until a field becomes visible: ```java slowLoadingField.waitUntilVisible() ``` You can also wait for more arbitrary conditions, e.g. ```java waitFor(ExpectedConditions.alertIsPresent()) ``` The default time that Serenity will wait is determined by the `webdriver.wait.for.timeout` property. The default value for this property is 5 seconds. Sometimes you want to give WebDriver some more time for a specific operation. From within a PageObject, you can override or extend the explicit timeout by using the withTimeoutOf() method. For example, you could wait for the #elements list to load for up to 5 seconds like this: ```java withTimeoutOf(5, SECONDS).waitForPresenceOf(By.cssSelector("#elements option")) ``` You can also specify the timeout for a field. For example, if you wanted to wait for up to 5 seconds for a button to become clickable before clicking on it, you could do the following: ```java someButton.withTimeoutOf(5, SECONDS).waitUntilClickable().click() ``` You can also use this approach to retrieve elements: ```java elements = withTimeoutOf(5, SECONDS).findAll("#elements option") ``` Finally, if a specific element a PageObject needs to have a bit more time to load, you can use the timeoutInSeconds attribute in the Serenity @FindBy annotation, e.g. ```java import net.serenitybdd.core.annotations.findby.FindBy; ... @FindBy(name = "country", timeoutInSeconds="10") public WebElementFacade country; ``` You can also wait for an element to be in a particular state, and then perform an action on the element. Here we wait for an element to be clickable before clicking on the element: ```java addToCartButton.withTimeoutOf(5, SECONDS).waitUntilClickable().click() ``` Or, you can wait directly on a web element: ```java @FindBy(id="share1-fb-like") WebElementFacade facebookIcon; ... public WebElementState facebookIcon() { return withTimeoutOf(5, TimeUnit.SECONDS).waitFor(facebookIcon); } ``` Or even: ```java List currencies = withTimeoutOf(5, TimeUnit.SECONDS) .waitFor(currencyTab) .thenFindAll(".currency-code"); ``` ## Executing Javascript There are times when you may find it useful to execute a little Javascript directly within the browser to get the job done. You can use the `evaluateJavascript()` method of the `PageObject` class to do this. For example, you might need to evaluate an expression and use the result in your tests. The following command will evaluate the document title and return it to the calling Java code: ```java String result = (String) evaluateJavascript("return document.title"); ``` Alternatively, you may just want to execute a Javascript command locally in the browser. In the following code, for example, we set the focus to the 'firstname' input field: ```java evaluateJavascript("document.getElementById('firstname').focus()"); ``` And, if you are familiar with JQuery, you can also invoke JQuery expressions: ```java evaluateJavascript("$('#firstname').focus()"); ``` This is often a useful strategy if you need to trigger events such as mouse-overs that are not currently supported by the WebDriver API. ## Uploading files Uploading files is easy. Files to be uploaded can be either placed in a hard-coded location (bad) or stored on the classpath (better). Here is a simple example: ```java public class NewCompanyPage extends PageObject { ... @FindBy(id="object_logo") WebElementFacade logoField; public NewCompanyPage(WebDriver driver) { super(driver); } public void loadLogoFrom(String filename) { upload(filename).to(logoField); } } ``` ## Using Fluent Matcher expressions When writing acceptance tests, you often find yourself expressing expectations about individual domain objects or collections of domain objects. For example, if you are testing a multi-criteria search feature, you will want to know that the application finds the records you expected. You might be able to do this in a very precise manner (for example, knowing exactly what field values you expect), or you might want to make your tests more flexible by expressing the ranges of values that would be acceptable. Serenity provides a few features that make it easier to write acceptance tests for this sort of case. In the rest of this section, we will study some examples based on tests for the Maven Central search site. This site lets you search the Maven repository for Maven artifacts, and view the details of a particular artifact. We will use some imaginary regression tests for this site to illustrate how the Serenity matchers can be used to write more expressive tests. The first scenario we will consider is simply searching for an artifact by name, and making sure that only artifacts matching this name appear in the results list. We might express this acceptance criteria informally in the following way: - Give that the developer is on the search page, - And the developer searches for artifacts called 'Serenity' - Then the developer should see at least 16 Serenity artifacts, each with a unique artifact Id In JUnit 5, a Serenity test for this scenario might look like the one: ```java ... import static net.thucydides.core.matchers.BeanMatchers.the_count; import static net.thucydides.core.matchers.BeanMatchers.each; import static net.thucydides.core.matchers.BeanMatchers.the; import static org.hamcrest.Matchers.greaterThanOrEqualTo; import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.startsWith; @ExtendWith(SerenityJUnit5Extension.class) class WhenSearchingForArtifacts { @Managed WebDriver driver; @Steps DeveloperSteps developer; @Test void should_find_the_right_number_of_artifacts() { developer.opens_the_search_page(); developer.searches_for("Serenity"); developer.should_see_artifacts_where(the("GroupId", startsWith("net.thucydides")), each("ArtifactId").isDifferent(), the_count(is(greaterThanOrEqualTo(16)))); } } ``` :::note JUnit 4 Deprecated If you're still using JUnit 4 with `@RunWith(SerenityRunner.class)`, note that JUnit 4 support is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 using `@ExtendWith(SerenityJUnit5Extension.class)`. ::: Let's see how the test in this class is implemented. The `should_find_the_right_number_of_artifacts()` test could be expressed as follows: . When we open the search page . And we search for artifacts containing the word 'Serenity' . Then we should see a list of artifacts where each Group ID starts with "net.Serenity", each Artifact ID is unique, and that there are at least 16 such entries displayed. The implementation of these steps is illustrated here: ```java ... import static net.thucydides.core.matchers.BeanMatcherAsserts.shouldMatch; public class DeveloperSteps { @Step public void opens_the_search_page() { onSearchPage().open(); } @Step public void searches_for(String search_terms) { onSearchPage().enter_search_terms(search_terms); onSearchPage().starts_search(); } @Step public void should_see_artifacts_where(BeanMatcher... matchers) { shouldMatch(onSearchResultsPage().getSearchResults(), matchers); } private SearchPage onSearchPage() { return getPages().get(SearchPage.class); } private SearchResultsPage onSearchResultsPage() { return getPages().get(SearchResultsPage.class); } } ``` The first two steps are implemented by relatively simple methods. However the third step is more interesting. Let's look at it more closely: ```java @Step public void should_see_artifacts_where(BeanMatcher... matchers) { shouldMatch(onSearchResultsPage().getSearchResults(), matchers); } ``` Here, we are passing an arbitrary number of expressions into the method. These expressions actually 'matchers', instances of the BeanMatcher class. Not that you usually have to worry about that level of detail - you create these matcher expressions using a set of static methods provided in the BeanMatchers class. So you typically would pass fairly readable expressions like `the("GroupId", startsWith("net.Serenity"))` or `each("ArtifactId").isDifferent()+. The `shouldMatch()` method from the BeanMatcherAsserts class takes either a single Java object, or a collection of Java objects, and checks that at least some of the objects match the constraints specified by the matchers. In the context of web testing, these objects are typically POJOs provided by the Page Object to represent the domain object or objects displayed on a screen. There are a number of different matcher expressions to choose from. The most commonly used matcher just checks the value of a field in an object. For example, suppose you are using the domain object shown here: ```java public class Person { private final String firstName; private final String lastName; Person(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() {...} public String getLastName() {...} } ``` You could write a test to ensure that a list of Persons contained at least one person named "Bill" by using the "the" static method, as shown here: ```java List persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor")); shouldMatch(persons, the("firstName", is("Bill")) ``` The second parameter in the the() method is a Hamcrest matcher, which gives you a great deal of flexibility with your expressions. For example, you could also write the following: ```java List persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor")); shouldMatch(persons, the("firstName", is(not("Tim")))); shouldMatch(persons, the("firstName", startsWith("B"))); ``` You can also pass in multiple conditions: ```java List persons = Arrays.asList(new Person("Bill", "Oddie"), new Person("Tim", "Brooke-Taylor")); shouldMatch(persons, the("firstName", is("Bill"), the("lastName", is("Oddie")); ``` Serenity also provides the DateMatchers class, which lets you apply Hamcrest matches to standard java Dates and `JodaTime` DateTimes. The following code samples illustrate how these might be used: ```java DateTime january1st2010 = new DateTime(2010,01,01,12,0).toDate(); DateTime may31st2010 = new DateTime(2010,05,31,12,0).toDate(); the("purchaseDate", isBefore(january1st2010)) the("purchaseDate", isAfter(january1st2010)) the("purchaseDate", isSameAs(january1st2010)) the("purchaseDate", isBetween(january1st2010, may31st2010)) ``` You sometimes also need to check constraints that apply to all of the elements under consideration. The simplest of these is to check that all of the field values for a particular field are unique. You can do this using the `each()` method: ```java shouldMatch(persons, each("lastName").isDifferent()) ``` You can also check that the number of matching elements corresponds to what you are expecting. For example, to check that there is only one person named Bill, you could do this: ```java shouldMatch(persons, the("firstName", is("Bill"), the_count(is(1))); ``` You can also check the minimum and maximum values using the max() and min() methods. For example, if the Person class had a `getAge()` method, we could ensure that every person is over 21 and under 65 by doing the following: ```java shouldMatch(persons, min("age", greaterThanOrEqualTo(21)), max("age", lessThanOrEqualTo(65))); ``` These methods work with normal Java objects, but also with Maps. So the following code will also work: ```java Map person = new HashMap(); person.put("firstName", "Bill"); person.put("lastName", "Oddie"); List> persons = Arrays.asList(person); shouldMatch(persons, the("firstName", is("Bill")) ``` The other nice thing about this approach is that the matchers play nicely with the Serenity reports. So when you use the BeanMatcher class as a parameter in your test steps, the conditions expressed in the step will be displayed in the test report. There are two common usage patterns when building Page Objects and steps that use this sort of matcher. The first is to write a Page Object method that returns the list of domain objects (for example, Persons) displayed on the table. For example, the getSearchResults() method used in the should_see_artifacts_where() step could be implemented as follows: ```java public List getSearchResults() { List rows = resultTable.findElements(By.xpath(".//tr[td]")); List artifacts = new ArrayList(); for (WebElement row : rows) { List cells = row.findElements(By.tagName("td")); artifacts.add(new Artifact(cells.get(0).getText(), cells.get(1).getText(), cells.get(2).getText())); } return artifacts; } ``` The second is to access the HTML table contents directly, without explicitly modelling the data contained in the table. This approach is faster and more effective if you don't expect to reuse the domain object in other pages. We will see how to do this next. ### Working with HTML Tables Since HTML tables are still widely used to represent sets of data on web applications, Serenity comes the HtmlTable class, which provides a number of useful methods that make it easier to write Page Objects that contain tables. For example, the rowsFrom method returns the contents of an HTML table as a list of Maps, where each map contains the cell values for a row indexed by the corresponding heading, as shown here: ```java ... import static net.thucydides.core.pages.components.HtmlTable.rowsFrom; public class SearchResultsPage extends PageObject { WebElement resultTable; public SearchResultsPage(WebDriver driver) { super(driver); } public List> getSearchResults() { return rowsFrom(resultTable); } } ``` This saves a lot of typing - our `getSearchResults()` method now looks like this: ```java public List> getSearchResults() { return rowsFrom(resultTable); } ``` And since the Serenity matchers work with both Java objects and Maps, the matcher expressions will be very similar. The only difference is that the Maps returned are indexed by the text values contained in the table headings, rather than by java-friendly property names. You can also read tables without headers (i.e., `` elements) by specifying your own headings using the `withColumns` method. For example: ```java List> tableRows = HtmlTable.withColumns("First Name","Last Name", "Favorite Colour") .readRowsFrom(page.table_with_no_headings); ``` You can also use the HtmlTable class to select particular rows within a table to work with. For example, another test scenario for the Maven Search page involves clicking on an artifact and displaying the details for that artifact. The test for this might look something like this: ```java @Test public void clicking_on_artifact_should_display_details_page() { developer.opens_the_search_page(); developer.searches_for("Serenity"); developer.open_artifact_where(the("ArtifactId", is("Serenity")), the("GroupId", is("net.Serenity"))); developer.should_see_artifact_details_where(the("artifactId", is("Serenity")), the("groupId", is("net.Serenity"))); } ``` Now the open_artifact_where() method needs to click on a particular row in the table. This step looks like this: ```java @Step public void open_artifact_where(BeanMatcher... matchers) { onSearchResultsPage().clickOnFirstRowMatching(matchers); } ``` So we are effectively delegating to the Page Object, who does the real work. The corresponding Page Object method looks like this: ```java import static net.thucydides.core.pages.components.HtmlTable.filterRows; ... public void clickOnFirstRowMatching(BeanMatcher... matchers) { List matchingRows = filterRows(resultTable, matchers); WebElement targetRow = matchingRows.get(0); WebElement detailsLink = targetRow.findElement(By.xpath(".//a[contains(@href,'artifactdetails')]")); detailsLink.click(); } ``` The interesting part here is the first line of the method, where we use the filterRows() method. This method will return a list of WebElements that match the matchers you have passed in. This method makes it fairly easy to select the rows you are interested in for special treatment. ## Switching to another page A method, switchToPage() is provided in PageObject class to make it convenient to return a new PageObject after navigation from within a method of a PageObject class. For example, ```java @DefaultUrl("http://mail.acme.com/login.html") public class EmailLoginPage extends PageObject { ... public void forgotPassword() { ... forgotPassword.click(); ForgotPasswordPage forgotPasswordPage = this.switchToPage(ForgotPasswordPage.class); forgotPasswordPage.open(); ... } ... } ``` ## WebElement collection loading strategies Selenium lets you use the `@FindBy` and `@FindAll` annotations to load collections of web elements, as illustrated here: ```java @FindBy(css='#colors a') List options ``` If you are working with an asynchronous application, these lists may take time to load, so Selenium may give you an empty list because the elements have not loaded yet. Serenity lets you find-tune this behaviour in two ways. The first is to use the wait DSL to load the elements directly, e.g.: ```java withTimeoutOf(5, SECONDS).waitForPresenceOf(By.cssSelector("#colors a")) ``` Alternatively, you can use the `serenity.webdriver.collection_loading_strategy` property to define how Serenity loads collections of web elements when using the `@FindBy` and `@FindAll` annotations. There are three options: - Optimistic - Pessimistic (default) - Paranoid Optimistic will only wait until the field is defined. This is the native Selenium behaviour. Pessimistic will wait until at least the first element is displayed. This is currently the default. Paranoid will wait until all of the elements are displayed. This can be slow for long lists. ## Working with fixture methods When a UI step fails in a Serenity test, the WebDriver instance is disabled for the rest of the test. This avoids unnecessary waits as the test steps through the subsequent steps (which it needs to do to document the test steps). The exception to this rule is in the case of fixture methods, such as methods annotated with the `@AfterEach` annotation in JUnit 5 (or `@After` in JUnit 4) or Cucumber. In these methods, the WebDriver instance can be used normally. In addition to the known JUnit and Cucumber annotations, any annotation starting with the word `After` will be considered a fixture method. For example, suppose you need to delete the customer accounts via the UI at the end of each test. You already have an `AdminSteps` step library with a `deleteAllCustomerAccounts()` method that performs this task. You could ensure that all of the customer accounts are deleted like this: ```java @Steps AdminSteps asAdministrator; ... @AfterEach void deleteUserAccounts() { asAdministrator.deleteAllCustomerAccounts(); } ``` This makes it easy to perform teardown or cleanup operations that use the user interface, potentially reusing steps or tasks that are used elsewhere in the tests. You can also override this behaviour at any point by calling the `reenableDrivers()` method, as shown here: ```java Serenity.webdriver().reenableDrivers(); ``` ## Extending Serenity WebDriver Integration Serenity offers a simple way to extend the default WebDriver capabilities and customise the driver creation and teardown activities. Simply implement the `BeforeAWebdriverScenario` and/or the `AfterAWebDriverScenario` interfaces (both are in the `net.serenitybdd.core.webdriver.enhancers` package). Serenity will execute `BeforeAWebdriverScenario` classes just before a driver instance is created, allowing you to add customised options to the driver capabilities. Any `AfterAWebDriverScenario` are executed at the end of a test, just before the driver is closed. ### The BeforeAWebdriverScenario interface The `BeforeAWebdriverScenario` is used to enhance the `Capabilities` object that will be passed to the WebDriver instance when a new driver is created. The method call passes in the requested driver and the `TestOutcome` object, which contains information about the name and tags used for this test. It also passes in the `EnvironmentVariables`, which gives you access to the current environment configuration. An example of a simple `BeforeAWebdriverScenario` is shown below: ```java public class MyCapabilityEnhancer implements BeforeAWebdriverScenario { @Override public DesiredCapabilities apply(EnvironmentVariables environmentVariables, SupportedWebDriver driver, TestOutcome testOutcome, MutableCapabilities capabilities) { capabilities.setCapability("name", testOutcome.getStoryTitle() + " - " + testOutcome.getTitle()); return capabilities; } } ``` ### The AfterAWebdriverScenario interface The `AfterAWebdriverScenario` is called at the end of a test, just before the driver is closed, and once the result of the test is known. The test result (and other details) can be obtained from the `TestOutcome` parameter. This allows any last manipulations or checks to be performed on the driver, before the end of the test. The following example checks the result of the test that has just finished, and adds a cookie with a value depending on the test outcome: ```java public class MyTestResultUpdater implements AfterAWebdriverScenario { void apply(EnvironmentVariables environmentVariables, TestOutcome testOutcome, WebDriver driver) { if ((driver == null) || (!RemoteDriver.isARemoteDriver(driver))) { return; } Cookie cookie = new Cookie("testPassed", testOutcome.isFailure() || testOutcome.isError() || testOutcome.isCompromised() ? "false" : "true"); driver.manage().addCookie(cookie); } } ``` ### Driver Enhancers If you need to perform some action on the driver instance before each test, you can implement the `CustomDriverEnhancer` class. Note that this will be called immediately after the driver is created, and before any page is opened, so the number of actions is relatively limited. ### Configuring the extension packages The last thing you need to do is to tell Serenity what package it needs to look for your extension classes. Add the package, or a parent package to your Serenity configuration using the `serenity.extension.packages`. `serenity.extension.packages=com.acme.myserenityextensions` You can find an example of how these classes are implemented in a real-world use case in the [serenity-browserstack](https://github.com/serenity-bdd/serenity-core/tree/master/serenity-browserstack/src/main/java/net/serenitybdd/browserstack) module on Github. ### Custom WebDriver implementations You can add your own custom WebDriver provider by implementing the DriverSource interface. First, you need to set up the following system properties (e.g. in your `serenity.properties` file): ``` webdriver.driver = provided webdriver.provided.type = mydriver webdriver.provided.mydriver = com.acme.MyPhantomJSDriver thucydides.driver.capabilities = mydriver ``` Your custom driver must implement the DriverSource interface, as shown here: ```java public class MyPhantomJSDriver implements DriverSource { @Override public WebDriver newDriver() { try { DesiredCapabilities capabilities = DesiredCapabilities.phantomjs(); // Add return new PhantomJSDriver(ResolvingPhantomJSDriverService.createDefaultService(), capabilities); } catch (IOException e) { throw new Error(e); } } @Override public boolean takesScreenshots() { return true; } } ``` Note that if you use custom drivers you will be entirely responsible for configuring and instantiating the browser instance, and the driver-related Serenity configuration options will not apply. We generally recommend using custom drivers only for very exceptional circumstances, and using `BeforeAWebdriverScenario` classes for most custom configuration requirements. ## Using the HasAuthentication interface Selenium 4 supports the `HasAuthentication` interface, which indicates that a driver supports authenticating to a website in some way. Serenity drivers integrate seemlessly with the `HasAuthentication` interface, so you can use it in the same way as you would in Selenium 4. For example, to register a username and password to use on all pages, you could add the following code before opening a site: ```java getDriver().register(UsernameAndPassword.of("scott", "tiger") ``` If authentication is only required on certain pages, you can also use a predicate: ```java Predicate onlyAuthenticateOnTheLoginPage = uri -> uri.getPath().endsWith("/login"); getDriver().register(onlyAuthenticateOnTheLoginPage, UsernameAndPassword.of("scott", "tiger")); ``` --- # Defining Requirements with Annotations Serenity BDD organises test results into a _requirements hierarchy_, which provides the structure for the Living Documentation reports. By default, this hierarchy is derived from the package structure (for JUnit tests) or from the directory structure (for Cucumber feature files). Starting with Serenity 5.3.1, you can define the requirements hierarchy directly using **annotations** on your test classes. This provides a simple, explicit, and declarative way to organise your tests into a meaningful structure, without having to rely on package naming conventions. ## The Requirements Hierarchy Serenity supports a three-level requirements hierarchy: ``` Epic > Feature > Story ``` Each level is represented by an annotation: | Annotation | Level | Purpose | |---|---|---| | `@Epic` | Highest | A large body of work, spanning multiple features | | `@Feature` | Middle | A coherent piece of functionality that users value | | `@Story` | Lowest | A specific user story or scenario group within a feature | You can use any combination of these levels. Not all three are required — a two-level structure (`@Feature` > `@Story`) or even a single level (`@Feature` alone) works just as well. ## Basic Usage ### Feature and Story The most common pattern is to annotate a test class with `@Feature` and `@Story`: ```java import net.serenitybdd.annotations.Feature; import net.serenitybdd.annotations.Story; @Feature("Managing Todos") @Story("Complete todo items") class WhenCompletingTodosTest extends SerenityPlaywrightTest { @Test void shouldMarkTodoAsCompleted() { // ... } @Test void shouldToggleAllTodosToCompleted() { // ... } } ``` This produces the following hierarchy in the requirements report: ``` Managing Todos (feature) └── Complete todo items (story) ├── Should mark todo as completed └── Should toggle all todos to completed ``` ### Multiple Stories Under the Same Feature Several test classes can share the same feature while defining different stories: ```java @Feature("Managing Todos") @Story("Complete todo items") class WhenCompletingTodosTest { /* ... */ } @Feature("Managing Todos") @Story("Delete todo items") class WhenDeletingTodosTest { /* ... */ } @Feature("Managing Todos") @Story("Filter todo items") class WhenFilteringTodosTest { /* ... */ } ``` This creates a single feature in the report with three child stories: ``` Managing Todos (feature) ├── Complete todo items (story) ├── Delete todo items (story) └── Filter todo items (story) ``` ### Multiple Features Different test classes can belong to different features: ```java @Feature("Creating Todos") @Story("Add todo items") class WhenAddingTodosTest { /* ... */ } @Feature("Managing Todos") @Story("Complete todo items") class WhenCompletingTodosTest { /* ... */ } ``` This produces two separate features in the report: ``` Creating Todos (feature) └── Add todo items (story) Managing Todos (feature) └── Complete todo items (story) ``` ## The Full Three-Level Hierarchy For larger projects, you can use `@Epic` to group related features: ```java import net.serenitybdd.annotations.Epic; import net.serenitybdd.annotations.Feature; import net.serenitybdd.annotations.Story; @Epic("E-Commerce Platform") @Feature("Shopping Cart") @Story("Add item to cart") class WhenAddingItemsToCartTest { /* ... */ } @Epic("E-Commerce Platform") @Feature("Shopping Cart") @Story("Remove item from cart") class WhenRemovingItemsFromCartTest { /* ... */ } @Epic("E-Commerce Platform") @Feature("Checkout") @Story("Pay with credit card") class WhenPayingWithCreditCardTest { /* ... */ } ``` This produces a three-level hierarchy: ``` E-Commerce Platform (epic) ├── Shopping Cart (feature) │ ├── Add item to cart (story) │ └── Remove item from cart (story) └── Checkout (feature) └── Pay with credit card (story) ``` ## Using @DisplayName as the Story Name When a test class has `@Feature` (or `@Epic`) but no `@Story` annotation, Serenity uses the JUnit 5 `@DisplayName` value as the story name. This is convenient when you want the display name in JUnit and the story name in Serenity to be the same: ```java @Feature("Managing Todos") @DisplayName("Complete todo items") class WhenCompletingTodosTest { @Test @DisplayName("should mark a todo as completed") void shouldMarkTodoAsCompleted() { // ... } } ``` This produces the same hierarchy as if you had written `@Story("Complete todo items")`: ``` Managing Todos (feature) └── Complete todo items (story) └── should mark a todo as completed ``` When both `@Story` and `@DisplayName` are present, `@Story` takes precedence for the requirements hierarchy, and `@DisplayName` is used only for the JUnit test runner display: ```java @Feature("Managing Todos") @Story("Complete todo items") @DisplayName("When completing todos") class WhenCompletingTodosTest { /* ... */ } ``` Here, the story name in the report is "Complete todo items" (from `@Story`), not "When completing todos" (from `@DisplayName`). :::tip When to use @Story vs @DisplayName Use `@Story` when you want the requirements hierarchy name to differ from the JUnit display name. For example, you might want a business-oriented name in the report ("Complete todo items") while using a BDD-style name in the test runner ("When completing todos"). Use `@DisplayName` alone (without `@Story`) when the same name works for both purposes. ::: ## Using @Feature Without @Story or @DisplayName If a test class has only `@Feature` with no `@Story` and no `@DisplayName`, the test class is placed directly under the feature. The humanised class name is used as the story name: ```java @Feature("Managing Todos") class WhenCompletingTodos { // Story name in report: "When completing todos" (from class name) } ``` ## Partial Hierarchies You don't need to use all three levels. Any subset works: ### Feature Only ```java @Feature("User Authentication") @Story("Login with valid credentials") class WhenLoggingInTest { /* ... */ } ``` ``` User Authentication (feature) └── Login with valid credentials (story) ``` ### Epic and Feature (No Story) ```java @Epic("Security") @Feature("User Authentication") @DisplayName("Login with valid credentials") class WhenLoggingInTest { /* ... */ } ``` ``` Security (epic) └── User Authentication (feature) └── Login with valid credentials (story) ``` ### Epic Only ```java @Epic("Security") @DisplayName("Login with valid credentials") class WhenLoggingInTest { /* ... */ } ``` ``` Security (epic) └── Login with valid credentials (story) ``` ## Inheriting Annotations from Superclasses Annotations are inherited from parent classes. This is useful when you have a common base class for a group of tests: ```java @Epic("TodoMVC Application") @Feature("Managing Todos") abstract class TodoManagementTest extends SerenityPlaywrightTest { // Common setup and utilities } @Story("Complete todo items") class WhenCompletingTodosTest extends TodoManagementTest { // Inherits @Epic and @Feature from parent } @Story("Delete todo items") class WhenDeletingTodosTest extends TodoManagementTest { // Inherits @Epic and @Feature from parent } ``` The most specific annotation wins. If a subclass redefines an annotation that is already present on the parent, the subclass annotation takes precedence: ```java @Feature("General Feature") abstract class BaseTest { } @Feature("Specific Feature") // Overrides parent's @Feature @Story("My Story") class SpecificTest extends BaseTest { } ``` ## Inheriting Annotations in JUnit 5 @Nested Classes When using JUnit 5 `@Nested` inner classes, annotations on the enclosing (outer) class are inherited by the nested class. This is a natural way to define the feature or epic once on the outer class and then define individual stories in each nested class: ```java @ExtendWith(SerenityJUnit5Extension.class) @Feature("Product Catalog") class ProductCatalogTests { @Nested @Story("Searching by keyword") class WhenSearchingByKeyword { @Test void shouldFindProductsByName() { /* ... */ } @Test void shouldReturnEmptyResultsForUnknownKeyword() { /* ... */ } } @Nested @Story("Browsing by category") class WhenBrowsingByCategory { @Test void shouldListProductsInCategory() { /* ... */ } } } ``` This produces the following hierarchy in the requirements report: ``` Product Catalog (feature) ├── Searching by keyword (story) │ ├── Should find products by name │ └── Should return empty results for unknown keyword └── Browsing by category (story) └── Should list products in category ``` ### Full Three-Level Hierarchy with Nested Classes You can combine `@Epic` and `@Feature` on the outer class with `@Story` on nested classes for the full hierarchy: ```java @ExtendWith(SerenityJUnit5Extension.class) @Epic("E-Commerce Platform") @Feature("Shopping Cart") class ShoppingCartTests { @Nested @Story("Add item to cart") class WhenAddingItems { @Test void shouldAddSingleItem() { /* ... */ } } @Nested @Story("Remove item from cart") class WhenRemovingItems { @Test void shouldRemoveSelectedItem() { /* ... */ } } } ``` This produces: ``` E-Commerce Platform (epic) └── Shopping Cart (feature) ├── Add item to cart (story) │ └── Should add single item └── Remove item from cart (story) └── Should remove selected item ``` ### Overriding Annotations in Nested Classes If a nested class redefines an annotation that is already present on the outer class, the nested class annotation takes precedence: ```java @Feature("Product Catalog") class ProductTests { @Nested @Feature("Checkout") // Overrides "Product Catalog" @Story("Express checkout") class WhenUsingExpressCheckout { /* ... */ } } ``` ### Using @DisplayName as Story Name in Nested Classes When a nested class has no `@Story` annotation, its `@DisplayName` is used as the story name, just as it is for top-level classes: ```java @Feature("Product Catalog") class ProductCatalogTests { @Nested @DisplayName("Searching by keyword") // Used as the story name class WhenSearchingByKeyword { /* ... */ } } ``` :::tip Nested classes vs separate test classes Use `@Nested` classes when you want to group related stories under a single outer class and share setup code. Use separate top-level test classes when the stories are more independent. Both approaches produce the same requirements hierarchy in the reports. ::: ## Annotations vs Package-Based Requirements Annotation-based requirements **override** the default package-based hierarchy for the annotated test class. This means you can mix both approaches in the same project: - Test classes with `@Feature`, `@Story`, or `@Epic` annotations use the annotation-based hierarchy - Test classes without these annotations fall back to the package-based hierarchy :::info When annotation-based and package-based hierarchies coexist in the same project, they appear as separate branches in the requirements tree. There is no merging between the two. ::: ## Annotations and Cucumber If you use both JUnit tests and Cucumber feature files in the same project, each group will have its own place in the requirements hierarchy: - **JUnit tests** with `@Feature`/`@Story` annotations create requirements based on the annotation values - **Cucumber features** create requirements based on the directory structure and feature file names To avoid confusion, use distinct names for annotation-based features and Cucumber features, or organise them into separate sections of your requirements hierarchy using `@Epic`. ## Working with the Screenplay Pattern The annotations work the same way with Screenplay-based tests: ```java import net.serenitybdd.annotations.Feature; import net.serenitybdd.annotations.Story; import net.serenitybdd.screenplay.ensure.Ensure; @Feature("Managing Todos") @Story("Filter todo items") @DisplayName("When filtering todos (Screenplay)") class WhenFilteringTodosScreenplayTest extends ScreenplayPlaywrightTest { @Test @DisplayName("should filter to show only active todos") void shouldFilterToShowOnlyActiveTodos() { toby.attemptsTo( FilterTodos.toShowActive(), Ensure.that(TheCurrentFilter.selected()).isEqualTo("Active"), Ensure.that(TheVisibleTodos.displayed()) .containsExactly("Buy milk", "Do laundry") ); } } ``` ## Tags In addition to organising the requirements hierarchy, the `@Epic`, `@Feature`, and `@Story` annotations also generate tags in the Serenity reports. A test annotated with: ```java @Epic("E-Commerce Platform") @Feature("Shopping Cart") @Story("Add item to cart") ``` will have the following tags in the report: - `epic:E-Commerce Platform` - `feature:Shopping Cart` - `story:Add item to cart` These tags can be used for filtering test results in the Serenity reports. ## Summary of Annotation Behaviour | Annotations Present | Story Name in Report | Requirements Path | |---|---|---| | `@Feature` + `@Story` | `@Story` value | Feature / Story | | `@Feature` + `@DisplayName` (no `@Story`) | `@DisplayName` value | Feature / DisplayName | | `@Feature` only | Humanised class name | Feature / class name | | `@Epic` + `@Feature` + `@Story` | `@Story` value | Epic / Feature / Story | | `@Epic` + `@Feature` + `@DisplayName` | `@DisplayName` value | Epic / Feature / DisplayName | | `@Epic` + `@Story` | `@Story` value | Epic / Story | | `@Epic` + `@DisplayName` | `@DisplayName` value | Epic / DisplayName | | `@Story` only | `@Story` value | Story | | No annotations + `@DisplayName` | `@DisplayName` value | Package-based path | ## Migration from Class-Based Annotations In earlier versions of Serenity, the `@Story` annotation required a class reference to define the requirements hierarchy: ```java // Legacy approach (still supported) public class MyApp { @Feature public class ShoppingCart { public class AddItem {} } } @Story(storyClass = MyApp.ShoppingCart.AddItem.class) class WhenAddingItemsTest { /* ... */ } ``` This approach required creating boilerplate marker classes and only supported a two-level hierarchy (Feature > Story). The new string-based annotations replace this with a simpler, more flexible approach: ```java // New approach @Feature("Shopping Cart") @Story("Add item to cart") class WhenAddingItemsTest { /* ... */ } ``` The legacy `@Story(storyClass = ...)` syntax is still supported for backwards compatibility. --- # Serenity Page Elements ## Introduction In most Selenium-based test automation frameworks, CSS or XPath selectors are commonly used to locate elements on a page. Serenity BDD provides a simpler way of interacting with common UI components, which can avoid needing to use XPath or CSS expressions. For example, the following HTML code illustrates a few different ways to represent a button: ```html ``` In Serenity, we could interact with these using XPath or CSS, e.g. ```java $("css:input[value='Login']").click() ``` or ```java $("xpath://button[.='Login']").click() ``` However, if the UI implementation switched from one to another, the test would break. Serenity Page Elements provide a more intuitive way to identify many common UI elements. Using Serenity Page Elements, we can use a more expression like this: ```java import net.serenitybdd.screenplay.ui.Button; . . . $(Button.withText("Login")).click(); ``` Or for a Screenplay test, you could do the following: ```java actor.attemptsTo( Click.on(Button.withText("Login")) ); ``` Under the hood, Serenity will look for either ` ``` We could click on this button with the following code: ```java $(Button.withNameOrId("login-button")).click(); ``` Or in Screenplay, we could do the following: ```java actor.attemptsTo( Click.on(Button.withNameOrId("login-button")) ); ``` In this case, Serenity will look for ` ``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By text | Button.withText("A Button") | | By value | Button.withText("Press Here") | | By name | Button.withNameOrId("submit-button") | | By data-test attribute | Button.withNameOrId("submit-the-button") | | By icon | Button.withIcon("glyphicon-home") | :::tip The `withNameOrId()` method will match the `id`, `name`, `data-test` or `aria-label` attribute for any kind of Page Element, so we won't always repeat them everywhere in the following sections. ::: ## Checkbox This element will match an `` element of type "checkbox", with a specified _value_, _data-test_, or _class_ attribute value. Suppose we have the following HTML: ```html


``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By id | Checkbox.withNameOrId("vehicle1") | | By value | Checkbox.withValue("Bike") | | By label | Checkbox.withLabel("I have a bike") | ## Dropdown The `Dropdown` page element lets us work with a ` ``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By name or id | Dropdown.withNameOrId("cars") | | By label | Dropdown.withLabel("Choose a car:") | | By default option | Dropdown.withLabel("---Pick Your Car---") | ## Image The `Image` page element lets us interact with `` elements. For example, suppose we had the following HTML code: ```html Girl in a jacket ``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By alt text | Image.withAltText("Girl in a jacket") | | By src | Image.withSrc("img_girl.jpg") | | By src (starting with) | Image.withSrcStartingWith("img_") | | By src (ending with) | Image.withSrcEndingWith("girl.jpg") | ## InputField The `InputField` page element represents an `` element in a form, with or without a `
` elements. For example, suppose we have the following HTML code: ```html Link 1
Link 2
Link 3
``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By link text | Link.withText("Link 1") | | By link title | Link.withTitle("Link Number 2") | | By partial link text | Link.containing("ink") | | By starting text | Link.startingWith("Link") | | By icon | Link.withIcon("glyphicon-cloud") | ## PageElement The `PageElement` page element represents any element on a page. For example, suppose we have the following HTML code: ```html

Item 1 heading

Item 1 description

Item 2 heading

Item 2 description
``` We could use the following expressions to identify elements in this code: | Strategy | Example | | ----------- | ----------- | | By id | PageElement.withNameOrId("container") | | By data-test value | PageElement.withNameOrId("the-container") | | Containing a text value | PageElement.withNameOrId("item").containingText("Item 1") | | Containing a text value (with a CSS locator) | PageElement.locatedBy(".item").containingText("Item 1") | ## RadioButton The `RadioButton` page element represents a set of radio buttons. For example, suppose we have the following HTML code: ```html



``` We could use the following expressions to identify these elements: | Strategy | Example | | ----------- | ----------- | | By id | RadioButton.withNameOrId("html") | | By label | RadioButton.withLabel("Choose CSS") | | By value |RadioButton.withValue("JavaScript") | # Using Page Elements in Action Classes You can also use Page Elements in your normal (non-Screenplay) tests, simply by using the Page Element expression as a locator. For example: ```java public class LoginActions extends UIInteractionSteps { @Step("Login as a standard user") public void asAStandardUser() { $(InputField.withPlaceholder("Username")).type("standard_user"); $(InputField.withPlaceholder("Password")).type("secret_sauce"); $(Button.withText("Login")).click(); } } ``` Page Elements can be used with all of the usual methods for locating elements in a UI Interaction class, including: | Method | Example | | ----------- | ----------- | | $ | $(Button.withText("Login")).click(); | | find | find(Button.withText("Login")).click(); | | $$ | $$(Checkbox.withNameOrId("vehicle-option")).forEach(WebElementFacade::click); | | findAll | findAll(Checkbox.withNameOrId("vehicle-option")).forEach(WebElementFacade::click); | --- Selenium WebDriver lets you run your tests in a variety of browsers, each with a variety of configuration options. In this section we will look at how to configure your WebDriver driver in Serenity. The simplest way to configure the driver you want to use is in your project's `serenity.conf` file (which you will find in `src/test/resources` folder). Basic configuration options go in the `webdriver` section. For example, to run your tests with Chrome, you set the `webdriver.driver` property to "chrome": ```hocon webdriver { driver = "chrome" } ``` All of the standard WebDriver browsers are supported: | Browser | Value | Example | | ----------- | ----------- | ---------------------------- | | Chrome | chrome | webdriver.driver = "chrome" | | Firefox | firefox | webdriver.driver = "firefox" | | Microsoft Edge | edge | webdriver.driver = "edge" | | Internet Explorer | IE | webdriver.driver = "IE" | | Safari | safari | webdriver.driver = "safari" | ## Configuring the WebDriver drivers When you run a WebDriver test against almost any driver, you need an OS-specific binary file to act as an intermediary between your test and the browser you want to manipulate. The main drivers, and where you can download them from, are listed below: | Browser | Driver | Location | System Property | | ------- | ------ | -------- | --------------- | | Chrome | chromedriver | http://chromedriver.chromium.org | webdriver.chrome.driver | | Firefox | geckodriver | https://github.com/mozilla/geckodriver/releases | webdriver.gecko.driver | | Microsoft Edge | msedgedriver | https://developer.microsoft.com/en-us/microsoft-edge/tools/webdriver/ | webdriver.edge.driver | | Internet Explorer | IEDriverServer | https://github.com/SeleniumHQ/selenium/wiki/InternetExplorerDriver | webdriver.ie.driver | ### Automatic driver downloads By default, Selenium automatically downloads and installs the appropriate driver binaries for the specified driver. ### Configuring driver binaries by hand If you cannot or do not want to download the WebDriver binaries automatically using WebDriverManager (for example, if you are in a corporate network which does not have access to the WebDriverManager binaries), you can download the binaries and configure them directly in the `serenity.conf` file. In this case you need to either have the correct driver binary on your system path, or provide the path to the binary using the system property shown in the table above. For example, your serenity.conf file might contain the following: ```hocon webdriver.gecko.driver=/path/to/my/geckodriver ``` However, adding a system path to your serenity.properties file is poor practice, as it means your tests will only run if the specified directory and binary exists, and that you are running the tests on the correct operating system. This obviously makes little sense if you are running your tests both locally, and on a CI environment. A more robust approach is to have your drivers in your source code, but have different drivers per OS. Serenity allows you to pass driver-specific properties to a driver, as long as they are prefixed with drivers._os_. For example, the following line will configure the webdriver.chrome.driver if you are running your tests under windows. ```hocon drivers { windows { webdriver.chrome.driver = src/test/resources/webdriver/windows/chromedriver.exe } ``` You can easily configure different binaries for different operating systems like this: ```hocon drivers { windows { webdriver.chrome.driver = src/test/resources/webdriver/windows/chromedriver.exe } mac { webdriver.chrome.driver = src/test/resources/webdriver/mac/chromedriver } linux { webdriver.chrome.driver = src/test/resources/webdriver/linux/chromedriver } } ``` This approach also works when you have more than one driver to configure. Suppose you need to run tests on three environments, using Firefox or Windows. One convenient approach is to store your drivers in a directory structure under src/test/resources similar to the following: ``` src/test/resources └── webdriver ├── linux │ ├── chromedriver │ └── geckodriver ├── mac │ ├── chromedriver │ └── geckkodriver └── windows ├── chromedriver.exe └── geckodriver.exe ``` This means that your tests will not need the webdriver binaries to be installed on every machine. The corresponding `serenity.conf` configuration for both browsers and each operating system would look like this: ```hocon drivers { windows { webdriver.chrome.driver = src/test/resources/webdriver/windows/chromedriver.exe webdriver.gecko.driver = src/test/resources/webdriver/windows/geckodriver.exe } mac { webdriver.chrome.driver = src/test/resources/webdriver/mac/chromedriver webdriver.gecko.driver = src/test/resources/webdriver/mac/geckodriver } linux { webdriver.chrome.driver = src/test/resources/webdriver/linux/chromedriver webdriver.gecko.driver = src/test/resources/webdriver/linux/geckodriver } } ``` ## Specifying W3C properties [W3C capabilities](https://www.w3.org/TR/webdriver/#capabilities) are a standard set of driver features that every drive implementation must support. You can configure W3C capabilities in the `wenbdriver.capabilities` section of your `serenity.conf` file, as shown here: ```hocon webdriver { capabilities { browserName = "Chrome" browserVersion = "103.0" platformName = "Windows 11" acceptInsecureCerts = true } } ``` You can define timeouts in a dedicated subsection like this: ```hocon webdriver { capabilities { browserName = "Chrome" browserVersion = "103.0" platformName = "Windows 11" timeouts { script = 30000 pageLoad = 300000 implicit = 2000 } } } ``` You can also define proxy configuration details in the `proxy` section: ```hocon webdriver { capabilities { browserName = "Chrome" browserVersion = "103.0" platformName = "Windows 11" proxy { proxyType = "30000" httpProxy = "myproxy.myorgcom:3128" } } } ``` ## Configuring Chrome # Configuring Selenium WebDriver [Previous content unchanged until Chrome configuration section...] ## Configuring Chrome You can use the special `"goog:chromeOptions"` capability to define any of the [ChromeDriver options](https://chromedriver.chromium.org/capabilities) ```hocon webdriver { capabilities { browserName = "Chrome" browserVersion = "103.0" platformName = "Windows 11" screenResolution = "1280x1024" "goog:chromeOptions" { args = [ "window-size=1000,800", "headless" ] binary = ${HOME}/path/to/chromedriver detach = true localState = { cart-contents = [1,2,3] } } } } ``` ### Configuring Chromedriver arguments ### Configuring proxy settings Chrome supports various proxy configurations that can be specified in your `serenity.conf` file. You can configure proxies in either the general capabilities section or within the Chrome-specific options: #### Basic Proxy Configuration ```hocon webdriver { capabilities { "goog:chromeOptions" { proxy { proxyType = "MANUAL" httpProxy = "proxy.example.com:8080" sslProxy = "proxy.example.com:8443" noProxy = "localhost,127.0.0.1,.example.com" } } } } ``` #### Proxy Types Serenity supports several proxy types: 1. Manual Proxy Configuration ```hocon "goog:chromeOptions" { proxy { proxyType = "MANUAL" httpProxy = "proxy.example.com:8080" # HTTP proxy sslProxy = "proxy.example.com:8443" # HTTPS proxy socksProxy = "socks.example.com:1080" # SOCKS proxy socksVersion = 5 # SOCKS version (4 or 5) noProxy = "localhost,127.0.0.1" # Bypass proxy for these addresses } } ``` 2. PAC (Proxy Auto-Configuration) ```hocon "goog:chromeOptions" { proxy { proxyType = "PAC" proxyAutoconfigUrl = "http://proxy.example.com/proxy.pac" } } ``` 3. System Proxy ```hocon "goog:chromeOptions" { proxy { proxyType = "SYSTEM" # Use system's proxy settings } } ``` 4. Direct Connection ```hocon "goog:chromeOptions" { proxy { proxyType = "DIRECT" # No proxy (direct connection) } } ``` 5. Auto-detect ```hocon "goog:chromeOptions" { proxy { proxyType = "AUTODETECT" # Auto-detect proxy settings } } ``` #### Proxy Authentication For proxies requiring authentication, include the credentials in the proxy URL: ```hocon "goog:chromeOptions" { proxy { proxyType = "MANUAL" httpProxy = "username:password@proxy.example.com:8080" } } ``` #### Environment-Specific Proxy Configuration You can configure different proxy settings for different environments: ```hocon environments { dev { webdriver.capabilities { "goog:chromeOptions" { proxy { proxyType = "DIRECT" } } } } qa { webdriver.capabilities { "goog:chromeOptions" { proxy { proxyType = "MANUAL" httpProxy = "proxy.qa.example.com:8080" sslProxy = "proxy.qa.example.com:8443" noProxy = "*.qa.example.com,localhost" } } } } } ``` ## Configuring Microsoft Edge Microsoft Edge is a Chromium driver, so the configuration is very similar to Chrome. The main difference is the use of `"ms:edgeOptions'"` instead of `"goog:chromeOptions"`. A typical configuration is shown below: ```hocon webdriver { capabilities { browserName = "MicrosoftEdge" "ms:edgeOptions" { args = ["test-type", "ignore-certificate-errors", "headless", "incognito", "disable-infobars", "disable-gpu", "disable-default-apps", "disable-popup-blocking"] } } } ``` ## Configuring Firefox Firefox uses the `"moz:firefoxOptions"` capability to define browser-specific option. A sample configuration is shown below: ```hocon webdriver { capabilities { browserName = "firefox" timeouts { implicit = 1000 script = 1000 pageLoad = 1000 } pageLoadStrategy = "normal" acceptInsecureCerts = true unhandledPromptBehavior = "dismiss" strictFileInteractability = true "moz:firefoxOptions" { args = ["-headless"], prefs { "javascript.options.showInConsole": false }, log {"level": "info"}, } } } ``` # Configuring multiple environments You can configure multiple driver configurations by using the `environments` section, as shown below. Then simply set the `environment` system property to the corresponding environment to use these settings, e.g. ``` mvn clean verify -Denvironment=chrome ``` A sample environments section is shown here: ``` environments { chrome { webdriver { driver = chrome autodownload = true capabilities { browserName = "chrome" acceptInsecureCerts = true "goog:chromeOptions" { args = ["test-type", "ignore-certificate-errors", "headless", "--window-size=1000,800" "incognito", "disable-infobars", "disable-gpu", "disable-default-apps", "disable-popup-blocking"] } } } } edge { webdriver { capabilities { browserName = "MicrosoftEdge" "ms:edgeOptions" { args = ["test-type", "ignore-certificate-errors", "headless", "incognito", "disable-infobars", "disable-gpu", "disable-default-apps", "disable-popup-blocking"] } } } } firefox { webdriver { capabilities { browserName = "firefox" pageLoadStrategy = "normal" acceptInsecureCerts = true unhandledPromptBehavior = "dismiss" strictFileInteractability = true "moz:firefoxOptions" { args = ["-headless"], prefs { "javascript.options.showInConsole": false }, log {"level": "info"}, } } } } } ``` --- # Running Cucumber scenarios with Serenity :::tip Comprehensive Cucumber Guide Available For a complete, in-depth guide on using Cucumber with Serenity BDD and JUnit 5, including configuration options, best practices, and troubleshooting, see the dedicated [Cucumber with JUnit 5](/docs/cucumber/cucumber-junit5) section. ::: To run Cucumber scenarios with Serenity, you need to create a Junit 5 test runner class like this: ```java import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("/features") public class CucumberTestSuite {} ``` Note that you need to specify at least the `features` attribute, to define the directory containing your feature files. # Feature hierarchies Serenity expects to find feature files in the `src/test/resources/features` folder by default. You can organise your feature files in a hierarchy of folders underneath this one, as shown here: ``` + src |-- test | |-- resources | | |-- features | | | |-- authentication | | | | |-- login.feature | | | |-- cart | | | | |-- managing_the_cart.feature | | | |-- catalog | | | | |-- browse_catalog.feature | | | | |-- filtering_products.feature | | | |-- purchases | | | | |-- completing_a_purchase.feature | | | |-- sales_tax | | | | |-- calculating_sales_tax.feature ``` The directory structure will be used to build the Serenity requirements hierarchy, like the one shown here: ![](img/cucumber-requirements-hierarchy.png) # Filtering test execution in Cucumber The simplest way to control which scenarios you want to execute is to use tags and the `cucumber.filter.tags` command line option. For example, to run only scenarios or features annotated with `@smoke`, you would run the following command: ``` mvn clean verify -Dcucumber.filter.tags="@smoke" ``` You can use [Cucumber Tag Expressions](https://cucumber.io/docs/cucumber/api/#tag-expressions) to determine which scenarios to run. For example, to run only features with both the `@authentication` tag and the `@smoke` tag, you would run the following: ``` mvn clean verify -Dcucumber.filter.tags="@authentication and @smoke" ``` To run scenarios with either the `@cart` or the `@catalog` tag, you could run the following: ``` mvn clean verify -Dcucumber.filter.tags="@cart or @catalog" ``` # Running Cucumber scenarios with Serenity in parallel Starting with version 3.6.0, is possible to run the Cucumber scenarios in parallel. The following steps are necessary to activate the parallel execution: 1. Add following dependencies in your application pom.xml: ```xml io.cucumber cucumber-junit-platform-engine 7.11.0 test org.junit.platform junit-platform-suite 1.9.2 test ``` 2. Create a file named `junit-platform.properties` under `test/resources` ``` cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 cucumber.execution.parallel.config.fixed.max-pool-size=4 cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel ``` :::note Breaking Change in Serenity 5.0.0 The Cucumber plugin path changed from `io.cucumber.core.plugin.*` to `net.serenitybdd.cucumber.core.plugin.*` in Serenity 5.0.0. Make sure to update your configuration files. ::: More informations about the meaning of the properties can be found in [Cucumber JUnit Platform Engine Documentation](https://github.com/cucumber/cucumber-jvm/tree/main/cucumber-junit-platform-engine) 3. Configure the classpath location of the scenarios to be executed in a Java class placed in a package under `test/java` ```java import org.junit.platform.suite.api.IncludeEngines; import org.junit.platform.suite.api.SelectClasspathResource; import org.junit.platform.suite.api.Suite; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("/features") public class CucumberTestSuite {} ``` --- # Running Serenity BDD Tests with JUnit 5 This guide covers everything you need to know to write and run Serenity BDD tests using JUnit 5 (Jupiter), without Cucumber. ## Overview JUnit 5 is the latest version of the popular Java testing framework. Combined with Serenity BDD, you get: - **Modern Test Features**: Parameterized tests, nested tests, dynamic tests, and more - **Powerful Assertions**: Built-in and third-party assertion libraries - **Rich Reporting**: Serenity's detailed reports with screenshots and step-by-step execution details - **Parallel Execution**: Run tests concurrently for faster feedback - **Flexible Organization**: Tags, nested classes, and display names for better test organization :::tip JUnit 5 Recommended JUnit 4 is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. All new projects should use JUnit 5. ::: ## Prerequisites - Java 17 or higher - Maven or Gradle - An IDE with Java support (IntelliJ IDEA, Eclipse, VS Code, etc.) ## Setting Up Dependencies ### Maven Dependencies Add the following to your `pom.xml`: ```xml 5.4.1 6.0.3 net.serenity-bdd serenity-junit5 ${serenity.version} test net.serenity-bdd serenity-screenplay ${serenity.version} test net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} test org.junit.jupiter junit-jupiter-api ${junit.version} test org.junit.jupiter junit-jupiter-engine ${junit.version} test org.assertj assertj-core 3.24.2 test ``` ### Gradle Dependencies For Gradle, add to your `build.gradle`: ```groovy dependencies { testImplementation "net.serenity-bdd:serenity-junit5:5.4.1" testImplementation "net.serenity-bdd:serenity-screenplay:5.4.1" testImplementation "net.serenity-bdd:serenity-screenplay-webdriver:5.4.1" testImplementation "org.junit.jupiter:junit-jupiter-api:6.0.3" testRuntimeOnly "org.junit.jupiter:junit-jupiter-engine:6.0.3" testImplementation "org.assertj:assertj-core:3.24.2" } test { useJUnitPlatform() } ``` ## Writing Your First Test ### Basic Test Structure Every Serenity JUnit 5 test must be annotated with `@ExtendWith(SerenityJUnit5Extension.class)`: ```java import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) class WhenSearchingForProducts { @Test void shouldFindProductByName() { // Your test code here } @Test void shouldFilterProductsByCategory() { // Your test code here } } ``` ### Web Test Example Here's a complete example of a web test: ```java import net.serenitybdd.annotations.Managed; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.openqa.selenium.WebDriver; import static org.assertj.core.api.Assertions.assertThat; @ExtendWith(SerenityJUnit5Extension.class) class WhenBrowsingProducts { @Managed(driver = "chrome", options = "headless") WebDriver driver; @Test void shouldDisplayProductDetails() { driver.get("https://example.com/products/123"); String productTitle = driver.findElement(By.id("product-title")).getText(); assertThat(productTitle).isEqualTo("Product Name"); } } ``` ### Using Step Libraries For better organization, use Serenity step libraries: ```java import net.serenitybdd.annotations.Steps; import net.serenitybdd.junit5.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @ExtendWith(SerenityJUnit5Extension.class) class WhenManagingShoppingCart { @Steps NavigationSteps navigation; @Steps ProductSteps products; @Steps CartSteps cart; @Test void shouldAddProductToCart() { navigation.openHomePage(); products.searchFor("laptop"); products.selectFirstProduct(); cart.addToCart(); cart.shouldContain(1, "items"); } } ``` ## Test Lifecycle ### Method-Level Lifecycle Use JUnit 5 lifecycle annotations: ```java import org.junit.jupiter.api.*; @ExtendWith(SerenityJUnit5Extension.class) class ProductTests { @BeforeEach void setUp() { // Runs before each test method System.out.println("Setting up test"); } @AfterEach void tearDown() { // Runs after each test method System.out.println("Cleaning up test"); } @BeforeAll static void setUpClass() { // Runs once before all tests in this class System.out.println("Setting up test class"); } @AfterAll static void tearDownClass() { // Runs once after all tests in this class System.out.println("Cleaning up test class"); } @Test void test1() { // Test code } @Test void test2() { // Test code } } ``` ### Execution Order ``` setUpClass() setUp() test1() tearDown() setUp() test2() tearDown() tearDownClass() ``` ## Display Names and Organization ### Custom Display Names Make test names more readable in reports: ```java import org.junit.jupiter.api.DisplayName; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("Shopping Cart Management") class ShoppingCartTests { @Test @DisplayName("Should add product to empty cart") void addToEmptyCart() { // Test code } @Test @DisplayName("Should update quantity of existing product") void updateQuantity() { // Test code } @Test @DisplayName("Should remove product from cart") void removeProduct() { // Test code } } ``` ### Organising Tests with @Feature and @Story You can use the `@Feature` and `@Story` annotations to organise your tests into a requirements hierarchy in the Serenity reports. This gives you explicit control over how tests appear in the Living Documentation, independent of the package structure: ```java import net.serenitybdd.annotations.Feature; import net.serenitybdd.annotations.Story; @ExtendWith(SerenityJUnit5Extension.class) @Feature("Shopping Cart") @Story("Add item to cart") @DisplayName("When adding items to the shopping cart") class WhenAddingItemsTest { @Test @DisplayName("Should add a single item") void addSingleItem() { /* ... */ } @Test @DisplayName("Should update quantity for duplicate items") void updateQuantityForDuplicates() { /* ... */ } } ``` When `@Story` is not present, the `@DisplayName` value is used as the story name in the requirements hierarchy. This means you can write: ```java @Feature("Shopping Cart") @DisplayName("Add item to cart") // Used as the story name class WhenAddingItemsTest { /* ... */ } ``` See [Annotation-Based Requirements](/docs/guide/annotation-requirements) for a comprehensive guide covering the full `@Epic` > `@Feature` > `@Story` hierarchy, inheritance, and migration from class-based annotations. ### Nested Tests Organize related tests with `@Nested`: ```java import org.junit.jupiter.api.Nested; @ExtendWith(SerenityJUnit5Extension.class) @DisplayName("User Authentication") class AuthenticationTests { @Nested @DisplayName("When logging in") class Login { @Test @DisplayName("Should succeed with valid credentials") void validCredentials() { // Test code } @Test @DisplayName("Should fail with invalid password") void invalidPassword() { // Test code } @Test @DisplayName("Should fail with non-existent user") void nonExistentUser() { // Test code } } @Nested @DisplayName("When logging out") class Logout { @Test @DisplayName("Should clear session") void clearSession() { // Test code } @Test @DisplayName("Should redirect to login page") void redirectToLogin() { // Test code } } } ``` #### Nested Classes and Requirements Annotations `@Nested` classes inherit `@Epic`, `@Feature`, and `@Story` annotations from their enclosing class. This lets you define the feature once on the outer class and assign individual stories to each nested class: ```java @ExtendWith(SerenityJUnit5Extension.class) @Feature("User Authentication") class AuthenticationTests { @Nested @Story("Login") class WhenLoggingIn { @Test void shouldSucceedWithValidCredentials() { /* ... */ } } @Nested @Story("Logout") class WhenLoggingOut { @Test void shouldClearSession() { /* ... */ } } } ``` This produces the requirements hierarchy: ``` User Authentication (feature) ├── Login (story) │ └── Should succeed with valid credentials └── Logout (story) └── Should clear session ``` See [Annotation-Based Requirements](/docs/guide/annotation-requirements#inheriting-annotations-in-junit-5-nested-classes) for the full details on annotation inheritance with nested classes. ## Parameterized Tests ### Simple Parameterized Tests ```java import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; @ExtendWith(SerenityJUnit5Extension.class) class SearchTests { @Steps SearchSteps search; @ParameterizedTest @ValueSource(strings = {"laptop", "phone", "tablet"}) @DisplayName("Should find products for search term: {0}") void shouldFindProducts(String searchTerm) { search.searchFor(searchTerm); search.shouldSeeResults(); } } ``` ### CSV Source ```java import org.junit.jupiter.params.provider.CsvSource; @ParameterizedTest @CsvSource({ "admin, admin123, Dashboard", "user, user123, User Home", "guest, guest123, Guest Portal" }) @DisplayName("Should login as {0} and see {2}") void shouldLoginSuccessfully(String username, String password, String expectedPage) { login.as(username, password); navigation.shouldBeOn(expectedPage); } ``` ### Method Source ```java import org.junit.jupiter.params.provider.MethodSource; import java.util.stream.Stream; @ParameterizedTest @MethodSource("provideTestData") @DisplayName("Should process order for {0}") void shouldProcessOrder(Order order) { checkout.process(order); checkout.shouldShowConfirmation(); } static Stream provideTestData() { return Stream.of( new Order("Product1", 2, 29.99), new Order("Product2", 1, 49.99), new Order("Product3", 5, 9.99) ); } ``` ## Tags and Filtering ### Tagging Tests Use `@Tag` to categorize tests: ```java import org.junit.jupiter.api.Tag; @ExtendWith(SerenityJUnit5Extension.class) class ProductTests { @Test @Tag("smoke") @Tag("fast") void quickSanityCheck() { // Fast smoke test } @Test @Tag("regression") @Tag("slow") void comprehensiveTest() { // Full regression test } @Test @Tag("wip") void workInProgress() { // Test under development } } ``` ### Running Tagged Tests **Maven:** ```bash # Run only smoke tests mvn test -Dgroups=smoke # Run smoke OR regression mvn test -Dgroups="smoke | regression" # Run smoke AND fast mvn test -Dgroups="smoke & fast" # Exclude wip tests mvn test -DexcludedGroups=wip ``` **junit-platform.properties:** ```properties junit.jupiter.includeTags=smoke junit.jupiter.excludeTags=wip,slow ``` ## Conditional Test Execution ### OS-Specific Tests ```java import org.junit.jupiter.api.condition.*; @Test @EnabledOnOs(OS.WINDOWS) void runOnlyOnWindows() { // Windows-specific test } @Test @DisabledOnOs(OS.MAC) void dontRunOnMac() { // Test disabled on macOS } ``` ### Java Version-Specific ```java @Test @EnabledOnJre(JRE.JAVA_17) void runOnlyOnJava17() { // Java 17 specific test } @Test @EnabledForJreRange(min = JRE.JAVA_17, max = JRE.JAVA_21) void runOnJava17To21() { // Modern Java versions } ``` ### Custom Conditions ```java @Test @EnabledIf("isProductionEnvironment") void runOnlyInProduction() { // Production-only test } boolean isProductionEnvironment() { return System.getProperty("env", "dev").equals("prod"); } ``` ## Assertions ### JUnit 5 Assertions ```java import static org.junit.jupiter.api.Assertions.*; @Test void demonstrateAssertions() { // Basic assertions assertEquals(expected, actual); assertTrue(condition); assertFalse(condition); assertNull(object); assertNotNull(object); // Array assertions assertArrayEquals(expectedArray, actualArray); // Exception assertions assertThrows(IllegalArgumentException.class, () -> { // Code that should throw exception }); // Timeout assertions assertTimeout(Duration.ofSeconds(2), () -> { // Code that should complete within 2 seconds }); // Grouped assertions assertAll("User validation", () -> assertEquals("John", user.getFirstName()), () -> assertEquals("Doe", user.getLastName()), () -> assertTrue(user.isActive()) ); } ``` ### AssertJ (Recommended) ```java import static org.assertj.core.api.Assertions.*; @Test void demonstrateAssertJ() { // Fluent assertions assertThat(actual).isEqualTo(expected); assertThat(list).hasSize(3) .contains("item1", "item2") .doesNotContain("item3"); // String assertions assertThat(text).startsWith("Hello") .endsWith("World") .contains("Test"); // Exception assertions assertThatThrownBy(() -> methodThatThrows()) .isInstanceOf(IllegalArgumentException.class) .hasMessage("Invalid input"); // Soft assertions (continue even after failures) SoftAssertions softly = new SoftAssertions(); softly.assertThat(user.getName()).isEqualTo("John"); softly.assertThat(user.getAge()).isGreaterThan(18); softly.assertAll(); } ``` ## Parallel Execution See the dedicated [Parallel Execution Guide](parallel-execution) for comprehensive coverage. Quick example in `junit-platform.properties`: ```properties # Enable parallel execution junit.jupiter.execution.parallel.enabled=true # Run test classes in parallel junit.jupiter.execution.parallel.mode.default=concurrent # Run test methods in same class sequentially junit.jupiter.execution.parallel.mode.classes.default=concurrent # Use dynamic strategy junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` ## Best Practices ### 1. Test Naming ```java // Good - Describes behavior @Test void shouldCalculateDiscountForRegularCustomer() { } // Better - Even more descriptive @Test @DisplayName("Should apply 10% discount for customers with 5+ orders") void regularCustomerDiscount() { } // Bad - Unclear purpose @Test void test1() { } ``` ### 2. Test Organization ```java // Good - Organized by feature/behavior @Nested @DisplayName("When user is logged in") class LoggedIn { @Nested @DisplayName("And viewing their profile") class ViewingProfile { @Test @DisplayName("Should see personal information") void seesPersonalInfo() { } @Test @DisplayName("Should be able to edit details") void canEditDetails() { } } } ``` ### 3. Use Step Libraries ```java // Good - Delegating to step libraries @Test void shouldCompleteCheckout() { cart.addProduct("laptop"); checkout.proceedToCheckout(); payment.payWithCreditCard("4111111111111111"); confirmation.shouldShowOrderNumber(); } // Bad - Low-level implementation in test @Test void checkout() { driver.findElement(By.id("add-to-cart")).click(); driver.findElement(By.id("checkout-button")).click(); // ... many more lines } ``` ### 4. Clean Test Data ```java @ExtendWith(SerenityJUnit5Extension.class) class OrderTests { private String orderId; @BeforeEach void createTestData() { orderId = testDataFactory.createOrder(); } @AfterEach void cleanupTestData() { testDataFactory.deleteOrder(orderId); } @Test void shouldProcessOrder() { // Test uses orderId } } ``` ### 5. Avoid Test Interdependence ```java // Bad - Tests depend on execution order @Test @Order(1) void createUser() { userId = userService.create("John"); } @Test @Order(2) void updateUser() { userService.update(userId, "Jane"); // Depends on test 1 } // Good - Each test is independent @Test void createUser() { String userId = userService.create("John"); assertThat(userId).isNotNull(); } @Test void updateUser() { String userId = userService.create("John"); // Create own data userService.update(userId, "Jane"); assertThat(userService.get(userId).getName()).isEqualTo("Jane"); } ``` ## Configuration ### Maven Surefire/Failsafe ```xml org.apache.maven.plugins maven-failsafe-plugin 3.0.0 **/*Test.java **/*Tests.java **/*TestCase.java ${webdriver.driver} integration-test verify ``` ### Serenity Properties Create `serenity.properties` in `src/test/resources`: ```properties # WebDriver configuration webdriver.driver=chrome webdriver.chrome.driver=path/to/chromedriver # Timeouts webdriver.timeouts.implicitlywait=2000 webdriver.wait.for.timeout=10000 # Reporting serenity.project.name=My Project serenity.test.root=net.example # Screenshots serenity.take.screenshots=FOR_FAILURES serenity.take.screenshots.for.tasks=BEFORE_AND_AFTER_EACH_STEP ``` ## Troubleshooting ### Common Issues **1. "No tests found"** - Ensure test class has `@ExtendWith(SerenityJUnit5Extension.class)` - Check test methods have `@Test` from `org.junit.jupiter.api` - Verify Maven/Gradle is configured to run JUnit 5 tests **2. "Multiple WebDriver instances"** - Use `@Managed` WebDriver - Serenity manages lifecycle - Don't create WebDriver instances manually - Ensure proper cleanup in `@AfterEach` if needed **3. "Tests fail in parallel but pass individually"** - Check for shared state between tests - Ensure test data is isolated - Review WebDriver management ## Next Steps - Explore [Parallel Execution](parallel-execution) for faster test runs - Learn about [Screenplay Pattern](/docs/screenplay/screenplay_fundamentals) for better test design - Review [Serenity Reports](/docs/reporting/the_serenity_reports) documentation - Check out [WebDriver Integration](/docs/guide/driver_config) for web testing ## Additional Resources - [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) - [Serenity BDD Official Site](https://serenity-bdd.info) - [AssertJ Documentation](https://assertj.github.io/doc/) - [Example Project](https://github.com/serenity-bdd/serenity-junit-screenplay-starter) --- # Running JUnit 5 Serenity Tests in Parallel Running JUnit 5 tests in parallel can significantly reduce test execution time. This guide explains how to configure and optimize parallel execution for Serenity BDD tests using JUnit 5. ## Overview JUnit 5 (Jupiter) has built-in support for parallel test execution. When combined with Serenity BDD, you can run test methods and test classes concurrently, taking advantage of multi-core processors. :::tip Parallel Execution Benefits On a machine with 8 cores, parallel execution can reduce test time from 30 minutes to 5-8 minutes or less. ::: ## Prerequisites - JUnit 5 (Jupiter) - Serenity BDD 3.6.0 or higher - Thread-safe test code - Sufficient system resources (CPU, RAM) ## Basic Configuration ### Step 1: Enable Parallel Execution Create or update `junit-platform.properties` in `src/test/resources`: ```properties # Enable parallel execution junit.jupiter.execution.parallel.enabled=true ``` ### Step 2: Configure Execution Mode ```properties # Run test classes in parallel junit.jupiter.execution.parallel.mode.default=concurrent # Run test classes in parallel junit.jupiter.execution.parallel.mode.classes.default=concurrent ``` ### Step 3: Configure Execution Strategy ```properties # Use dynamic strategy (recommended) junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` ## Execution Modes ### Concurrent Mode Tests run in parallel: ```properties junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.mode.classes.default=concurrent ``` ### Same Thread Mode Tests run sequentially: ```properties junit.jupiter.execution.parallel.mode.default=same_thread junit.jupiter.execution.parallel.mode.classes.default=same_thread ``` ### Mixed Mode Run classes in parallel but methods within a class sequentially: ```properties # Classes in parallel junit.jupiter.execution.parallel.mode.classes.default=concurrent # Methods in same class run sequentially junit.jupiter.execution.parallel.mode.default=same_thread ``` ## Execution Strategies ### Dynamic Strategy (Recommended) Automatically determines thread count based on available processors: ```properties junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` **Factor calculation:** `threads = processors × factor` **Examples** (on an 8-core machine): - Factor `1.0` = 8 threads (recommended) - Factor `0.5` = 4 threads - Factor `2.0` = 16 threads ### Fixed Strategy Uses a specified number of threads: ```properties junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=4 ``` **Use when:** - You need consistent thread counts - Running on shared CI/CD infrastructure - Debugging parallel execution issues ### Custom Strategy Implement your own strategy: ```properties junit.jupiter.execution.parallel.config.strategy=custom junit.jupiter.execution.parallel.config.custom.class=com.example.MyParallelStrategy ``` ## Controlling Parallelism at Class/Method Level ### Per-Class Configuration Use `@Execution` annotation to control parallelism: ```java import org.junit.jupiter.api.parallel.Execution; import org.junit.jupiter.api.parallel.ExecutionMode; @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.CONCURRENT) // Run this class's methods in parallel class FastTests { @Test void test1() { } @Test void test2() { } @Test void test3() { } } ``` ### Force Sequential Execution ```java @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.SAME_THREAD) // Run sequentially class DatabaseMigrationTests { @Test void migration1() { } @Test void migration2() { } // Must run after migration1 } ``` ### Method-Level Control ```java @ExtendWith(SerenityJUnit5Extension.class) class MixedTests { @Test @Execution(ExecutionMode.CONCURRENT) void canRunInParallel() { } @Test @Execution(ExecutionMode.SAME_THREAD) void mustRunSequentially() { } } ``` ## Resource Locks Prevent concurrent access to shared resources: ```java import org.junit.jupiter.api.parallel.ResourceLock; @ExtendWith(SerenityJUnit5Extension.class) class SharedResourceTests { @Test @ResourceLock("database") void accessDatabase1() { // Exclusive access to database } @Test @ResourceLock("database") void accessDatabase2() { // Will wait for accessDatabase1 to complete } @Test @ResourceLock(value = "database", mode = READ) void readFromDatabase() { // Read-only access - can run concurrently with other reads } } ``` ### Common Resource Locks ```java import static org.junit.jupiter.api.parallel.Resources.*; @Test @ResourceLock(SYSTEM_PROPERTIES) void modifiesSystemProperties() { } @Test @ResourceLock(SYSTEM_OUT) void writesToSystemOut() { } @Test @ResourceLock("my-custom-resource") void accessesCustomResource() { } ``` ## Thread Safety with Serenity ### WebDriver Management Serenity's `@Managed` WebDriver is automatically thread-safe: ```java @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.CONCURRENT) class WebTests { @Managed(driver = "chrome", options = "headless") WebDriver driver; // Each thread gets its own instance @Test void test1() { driver.get("https://example.com"); // Thread-safe } @Test void test2() { driver.get("https://example.com/other"); // Independent WebDriver instance } } ``` ### Step Libraries Step libraries are thread-safe when properly used: ```java @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.CONCURRENT) class CheckoutTests { @Steps CartSteps cart; // Thread-safe - instance per thread @Test void addToCart() { cart.addProduct("Product1"); } @Test void removeFromCart() { cart.addProduct("Product2"); cart.removeProduct("Product2"); } } ``` ### Avoid Shared Mutable State ```java // ❌ BAD - Not thread-safe @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.CONCURRENT) class BadTests { private String sharedData; // Shared mutable state! @Test void test1() { sharedData = "test1"; // Race condition! } @Test void test2() { sharedData = "test2"; // Race condition! } } // ✅ GOOD - Thread-safe @ExtendWith(SerenityJUnit5Extension.class) @Execution(ExecutionMode.CONCURRENT) class GoodTests { @Test void test1() { String localData = "test1"; // Local variable - thread-safe // Use localData } @Test void test2() { String localData = "test2"; // Independent // Use localData } } ``` ## Complete Configuration Example ### Comprehensive junit-platform.properties ```properties # ========================================== # Parallel Execution # ========================================== junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.mode.classes.default=concurrent # ========================================== # Execution Strategy # ========================================== junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 # Alternative: Fixed strategy # junit.jupiter.execution.parallel.config.strategy=fixed # junit.jupiter.execution.parallel.config.fixed.parallelism=4 # junit.jupiter.execution.parallel.config.fixed.max-pool-size=8 # ========================================== # Test Instance Lifecycle # ========================================== junit.jupiter.testinstance.lifecycle.default=per_method # ========================================== # Test Discovery # ========================================== junit.jupiter.testclass.order.default=org.junit.jupiter.api.ClassOrderer$Random # ========================================== # Display Names # ========================================== junit.jupiter.displayname.generator.default=org.junit.jupiter.api.DisplayNameGenerator$ReplaceUnderscores ``` ## Maven Configuration ### Failsafe Plugin Configure Maven Failsafe plugin for parallel execution: ```xml org.apache.maven.plugins maven-failsafe-plugin 3.0.0 **/*Test.java **/*Tests.java ${webdriver.driver} ${environment} -Xmx2g integration-test verify ``` ### Running from Command Line ```bash # Run all tests in parallel mvn clean verify # Override parallelism mvn clean verify -Djunit.jupiter.execution.parallel.config.fixed.parallelism=8 # Disable parallel execution mvn clean verify -Djunit.jupiter.execution.parallel.enabled=false ``` ## Gradle Configuration ```groovy test { useJUnitPlatform() // Enable parallel execution maxParallelForks = Runtime.runtime.availableProcessors() // Increase memory minHeapSize = "512m" maxHeapSize = "2g" // System properties systemProperty 'junit.jupiter.execution.parallel.enabled', 'true' systemProperty 'junit.jupiter.execution.parallel.config.strategy', 'dynamic' systemProperty 'junit.jupiter.execution.parallel.config.dynamic.factor', '1.0' } ``` ## Performance Optimization ### 1. Choose the Right Strategy **Small test suites** (<30 tests): ```properties junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=2 ``` **Medium test suites** (30-100 tests): ```properties junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` **Large test suites** (>100 tests): ```properties junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=0.75 ``` ### 2. Monitor Resource Usage ```bash # Monitor while tests run top htop jconsole ``` Watch for: - **CPU**: Should be near 100% utilization - **Memory**: Each browser instance uses ~200-500MB RAM - **Threads**: Should match your parallelism setting ### 3. Optimize Test Design ```java // ✅ GOOD - Fast, focused tests @Test void shouldCalculateTotal() { // No external dependencies, runs quickly assertThat(calculator.add(2, 3)).isEqualTo(5); } // ⚠️ SLOW - Consider optimization @Test void shouldProcessLargeDataset() { // Process 10000 records - consider using smaller dataset } ``` ### 4. Balance Thread Count Too few threads: ```properties # Underutilizing CPU junit.jupiter.execution.parallel.config.dynamic.factor=0.25 ``` Too many threads: ```properties # May cause resource exhaustion junit.jupiter.execution.parallel.config.dynamic.factor=4.0 ``` Optimal: ```properties # Matches CPU cores junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` ## Debugging Parallel Tests ### Temporarily Disable Parallelism ```properties junit.jupiter.execution.parallel.enabled=false ``` Or via command line: ```bash mvn verify -Djunit.jupiter.execution.parallel.enabled=false ``` ### Run Single Test Class ```bash mvn test -Dtest=MyTestClass ``` ### Enable Verbose Logging Add to `junit-platform.properties`: ```properties # Show which thread runs which test junit.platform.output.capture.stdout=true junit.platform.output.capture.stderr=true ``` In your test: ```java @BeforeEach void logThread() { System.out.println("Running on: " + Thread.currentThread().getName()); } ``` ## Common Issues and Solutions ### Issue 1: Flaky Tests **Symptoms:** Tests pass individually but fail in parallel **Solutions:** - Check for shared mutable state - Verify test data isolation - Review resource locking - Add proper synchronization ```java // Problem: Shared state private static int counter = 0; // ❌ Not thread-safe @Test void incrementCounter() { counter++; // Race condition } // Solution: Use atomic types or local variables private static AtomicInteger counter = new AtomicInteger(0); // ✅ Thread-safe @Test void incrementCounter() { counter.incrementAndGet(); } ``` ### Issue 2: Resource Exhaustion **Symptoms:** Tests slow down or fail after some time **Solutions:** - Reduce thread count - Increase JVM heap size - Check for resource leaks - Monitor system resources ```xml -Xmx4g ``` ### Issue 3: WebDriver Issues **Symptoms:** Browser instances multiply or don't close **Solutions:** - Use `@Managed` WebDriver (Serenity handles lifecycle) - Ensure proper cleanup in `@AfterEach` - Limit concurrent browser instances ```java // ✅ Serenity manages lifecycle @Managed WebDriver driver; // ❌ Don't manage manually in parallel tests WebDriver driver = new ChromeDriver(); // Memory leak risk ``` ### Issue 4: Database Locks **Symptoms:** Tests timeout waiting for database **Solutions:** - Use resource locks - Isolate test data - Use in-memory database for tests ```java @Test @ResourceLock("database") void modifiesDatabase() { // Exclusive database access } ``` ## Best Practices ### 1. Start Small Begin with 2-4 threads and gradually increase: ```properties junit.jupiter.execution.parallel.config.strategy=fixed junit.jupiter.execution.parallel.config.fixed.parallelism=2 ``` ### 2. Isolate Test Data ```java @ExtendWith(SerenityJUnit5Extension.class) class UserTests { @Test void createUser() { String uniqueId = UUID.randomUUID().toString(); User user = createTestUser("user_" + uniqueId); // Test with unique user } } ``` ### 3. Use Tags for Selective Parallelism ```java @Test @Tag("parallel") void canRunInParallel() { } @Test @Tag("serial") @Execution(ExecutionMode.SAME_THREAD) void mustRunSequentially() { } ``` ```properties # Run only parallel tests junit.jupiter.includeTags=parallel ``` ### 4. Clean Up Resources ```java @AfterEach void cleanup() { // Clean up test data testDataService.deleteTestData(testId); } ``` ### 5. Document Thread Safety ```java /** * Tests in this class modify global application state * and must run sequentially. */ @Execution(ExecutionMode.SAME_THREAD) class GlobalStateTests { // ... } ``` ## Measuring Performance ### Before Parallel Execution ``` Total tests: 150 Execution time: 25 minutes ``` ### After Parallel Execution (8 threads) ``` Total tests: 150 Execution time: 4 minutes Speedup: 6.25x Efficiency: 78% ``` **Calculate metrics:** ``` Speedup = Sequential Time / Parallel Time Efficiency = Speedup / Number of Threads × 100% ``` ## Next Steps - Return to the main [JUnit 5 Guide](junit5-tests) - Learn about [Test Organization](junit5-tests#display-names-and-organization) - Review [Best Practices](junit5-tests#best-practices) - Explore [Serenity Reports](/docs/reporting/the_serenity_reports) ## Additional Resources - [JUnit 5 Parallel Execution Docs](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution) - [Serenity BDD Performance Tips](https://serenity-bdd.info) - [Java Concurrency in Practice](https://jcip.net/) --- # Running Cucumber Scenarios with Serenity BDD and JUnit 5 Serenity BDD provides seamless integration with Cucumber, allowing you to write executable specifications in Gherkin and run them using JUnit 5. This guide covers everything you need to know to use Cucumber with Serenity BDD and JUnit 5 effectively. ## Overview Cucumber is a popular BDD (Behavior-Driven Development) tool that allows you to write test scenarios in plain language using the Gherkin syntax. When combined with Serenity BDD and JUnit 5, you get: - **Living Documentation**: Serenity generates rich, narrative reports from your Cucumber scenarios - **Modern Test Execution**: JUnit 5's powerful platform for running tests - **Parallel Execution**: Run scenarios in parallel for faster test execution - **Rich Reporting**: Detailed test reports with screenshots, stack traces, and more ## Prerequisites Before you start, ensure you have: - Java 17 or higher - Maven or Gradle - An IDE with Java support (IntelliJ IDEA, Eclipse, VS Code, etc.) ## Setting Up Dependencies ### Maven Dependencies Add the following dependencies to your `pom.xml`: ```xml 5.4.1 7.34.2 6.0.3 net.serenity-bdd serenity-cucumber ${serenity.version} test io.cucumber cucumber-junit-platform-engine ${cucumber.version} test org.junit.platform junit-platform-suite ${junit.version} test ``` ### Gradle Dependencies For Gradle, add to your `build.gradle`: ```groovy dependencies { testImplementation "net.serenity-bdd:serenity-cucumber:5.4.1" testImplementation "io.cucumber:cucumber-junit-platform-engine:7.34.2" testImplementation "org.junit.platform:junit-platform-suite:6.0.3" } ``` ## Creating a Basic Test Runner The simplest way to run Cucumber scenarios with JUnit 5 is to create a test suite class: ```java import org.junit.platform.suite.api.*; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") class CucumberTestSuite { } ``` This minimal configuration: - `@Suite` - Marks this as a JUnit Platform Suite - `@IncludeEngines("cucumber")` - Tells JUnit to use the Cucumber engine - `@SelectClasspathResource("features")` - Points to the directory containing your feature files ## Configuring the Serenity Reporter :::warning Breaking Change in Serenity 5.0.0 The Serenity Cucumber plugin path changed in version 5.0.0 from `io.cucumber.core.plugin.*` to `net.serenitybdd.cucumber.core.plugin.*` ::: To generate Serenity reports, you must configure the Serenity reporter plugin. Add this to your test suite: ```java import org.junit.platform.suite.api.*; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) class CucumberTestSuite { } ``` The `SerenityReporterParallel` plugin is thread-safe and recommended for all scenarios, even if you're not running tests in parallel. ## Configuration Options ### Using @ConfigurationParameter Annotations You can configure various Cucumber options using `@ConfigurationParameter` annotations. Here are the most commonly used: #### Specifying Step Definition Packages ```java import static io.cucumber.junit.platform.engine.Constants.GLUE_PROPERTY_NAME; @ConfigurationParameter( key = GLUE_PROPERTY_NAME, value = "com.example.stepdefinitions" ) ``` #### Filtering by Tags ```java import static io.cucumber.junit.platform.engine.Constants.FILTER_TAGS_PROPERTY_NAME; @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "@smoke and not @wip" ) ``` #### Specifying Feature Paths ```java import static io.cucumber.junit.platform.engine.Constants.FEATURES_PROPERTY_NAME; @ConfigurationParameter( key = FEATURES_PROPERTY_NAME, value = "src/test/resources/features" ) ``` #### Complete Example ```java import org.junit.platform.suite.api.*; import static io.cucumber.junit.platform.engine.Constants.*; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) @ConfigurationParameter( key = GLUE_PROPERTY_NAME, value = "com.example.stepdefinitions" ) @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "@smoke" ) class SmokeTestSuite { } ``` ### Using junit-platform.properties Alternatively, you can configure Cucumber options in a `junit-platform.properties` file located in `src/test/resources`: ```properties # Enable Cucumber cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel cucumber.glue=com.example.stepdefinitions cucumber.filter.tags=@smoke cucumber.features=src/test/resources/features # Scenario execution order cucumber.execution.order=random # Plugin output cucumber.plugin=pretty,html:target/cucumber-reports/cucumber.html ``` :::tip When to Use Each Approach - Use `@ConfigurationParameter` for **suite-specific** configuration (e.g., different test suites for smoke, regression, etc.) - Use `junit-platform.properties` for **project-wide** default configuration - Annotations override properties file settings ::: ## Organizing Feature Files ### Directory Structure Serenity expects feature files in `src/test/resources/features` by default. Organize them hierarchically by feature area: ``` src/test/resources/features/ ├── authentication/ │ ├── login.feature │ └── logout.feature ├── shopping/ │ ├── add_to_cart.feature │ ├── checkout.feature │ └── payment.feature └── search/ ├── basic_search.feature └── advanced_filters.feature ``` This hierarchy will be reflected in your Serenity reports, creating a clear requirements structure. ### Feature File Example ```gherkin @shopping @checkout Feature: Checkout Process As a customer I want to complete my purchase So that I can receive my items Background: Given I have items in my cart @happy-path @smoke Scenario: Successful checkout with credit card When I proceed to checkout And I enter valid shipping information And I pay with a valid credit card Then I should see an order confirmation And I should receive a confirmation email @edge-case Scenario: Checkout with invalid credit card When I proceed to checkout And I enter valid shipping information And I pay with an invalid credit card Then I should see a payment error message And my order should not be placed ``` ## Writing Step Definitions Step definitions connect your Gherkin scenarios to Java code: ```java package com.example.stepdefinitions; import io.cucumber.java.en.*; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.actors.OnStage; public class CheckoutStepDefinitions { @Given("{actor} has items in the cart") public void hasItemsInCart(Actor actor) { actor.attemptsTo( // Your implementation here ); } @When("{actor} proceeds to checkout") public void proceedsToCheckout(Actor actor) { actor.attemptsTo( // Your implementation here ); } @Then("{actor} should see an order confirmation") public void shouldSeeOrderConfirmation(Actor actor) { actor.should( // Your assertions here ); } } ``` ### Step Definition Best Practices 1. **Keep steps reusable** - Write generic steps that can be used in multiple scenarios 2. **Use the Screenplay pattern** - For better maintainability and readability 3. **Avoid step interdependence** - Each step should be independent 4. **Use parameters** - Make steps flexible with Cucumber expressions ## Filtering Scenarios with Tags ### Tag Expressions Cucumber supports powerful tag expressions for filtering scenarios: ```bash # Run scenarios with @smoke tag mvn clean verify -Dcucumber.filter.tags="@smoke" # Run scenarios with both tags mvn clean verify -Dcucumber.filter.tags="@smoke and @regression" # Run scenarios with either tag mvn clean verify -Dcucumber.filter.tags="@smoke or @regression" # Exclude scenarios mvn clean verify -Dcucumber.filter.tags="not @wip" # Complex expressions mvn clean verify -Dcucumber.filter.tags="(@smoke or @regression) and not @slow" ``` :::caution Gradle ignores command-line `-D` options in the test JVM `gradle test -Dcucumber.filter.tags=@smoke` runs every scenario, because Gradle does not pass `-D` options to the JVM that runs the tests. Forward them in `build.gradle`: ```groovy test { systemProperties System.properties.findAll { k, v -> k.toString().startsWith('cucumber.') } } ``` ::: :::caution Name the runner class so Maven runs it Maven's test plugin only runs classes named `*Test`, `*Tests`, `Test*` or `*TestCase`. A runner called `CucumberSuite` or `CucumberTestSuite` is skipped without a warning, and the build ends with `Results: 0 tests`. Call it `RunCucumberTests` (or similar), or add the name to the `includes` of the surefire plugin. ::: ### In Code ```java @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "(@smoke or @regression) and not @wip" ) ``` ### In Properties File ```properties cucumber.filter.tags=(@smoke or @regression) and not @wip ``` ## Running Scenarios in Parallel See the dedicated [Parallel Execution](parallel-execution) guide for detailed information on running Cucumber scenarios in parallel. Quick example: **junit-platform.properties:** ```properties cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel ``` ## Cucumber Hooks Cucumber hooks allow you to run code before or after scenarios: ```java package com.example.hooks; import io.cucumber.java.*; public class CucumberHooks { @Before public void beforeScenario(Scenario scenario) { System.out.println("Starting scenario: " + scenario.getName()); } @After public void afterScenario(Scenario scenario) { System.out.println("Finished scenario: " + scenario.getName()); if (scenario.isFailed()) { // Handle failure } } @BeforeStep public void beforeStep() { // Runs before each step } @AfterStep public void afterStep() { // Runs after each step } } ``` ### Tagged Hooks Run hooks only for specific tags: ```java @Before("@database") public void setupDatabase() { // Only runs for scenarios tagged with @database } @After("@cleanup") public void cleanup() { // Only runs for scenarios tagged with @cleanup } ``` ### Hook Execution Order Control hook execution order with the `order` parameter: ```java @Before(order = 1) public void firstHook() { // Runs first } @Before(order = 2) public void secondHook() { // Runs second } ``` ## Data Tables Cucumber data tables allow you to pass structured data to steps: ### Feature File: ```gherkin Scenario: Create multiple users Given the following users exist: | username | email | role | | alice | alice@example.com | admin | | bob | bob@example.com | user | | charlie | charlie@example.com| user | ``` ### Step Definition: ```java @Given("the following users exist:") public void createUsers(DataTable dataTable) { List> users = dataTable.asMaps(); for (Map user : users) { String username = user.get("username"); String email = user.get("email"); String role = user.get("role"); // Create user } } ``` ## Scenario Outlines Scenario outlines allow you to run the same scenario with different data: ```gherkin Scenario Outline: Login with different credentials Given I am on the login page When I enter username "" and password "" Then I should see "" Examples: | username | password | result | | admin | admin123 | Dashboard | | user | user123 | User Home | | invalid | wrong | Invalid credentials | ``` ## Configuration Parameters Reference ### Common Cucumber Constants All available from `io.cucumber.junit.platform.engine.Constants`: | Constant | Purpose | Example | |----------|---------|---------| | `PLUGIN_PROPERTY_NAME` | Configure plugins | `"net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel"` | | `GLUE_PROPERTY_NAME` | Step definition packages | `"com.example.stepdefinitions"` | | `FILTER_TAGS_PROPERTY_NAME` | Tag expression filter | `"@smoke and not @wip"` | | `FEATURES_PROPERTY_NAME` | Feature file locations | `"src/test/resources/features"` | | `PLUGIN_PUBLISH_QUIET_PROPERTY_NAME` | Suppress Cucumber publish messages | `"true"` | | `ANSI_COLORS_DISABLED_PROPERTY_NAME` | Disable colored output | `"true"` | | `EXECUTION_DRY_RUN_PROPERTY_NAME` | Check step definitions without running | `"true"` | | `OBJECT_FACTORY_PROPERTY_NAME` | Custom object factory | `"com.example.CustomObjectFactory"` | ### Parallel Execution Properties | Property | Purpose | Example | |----------|---------|---------| | `cucumber.execution.parallel.enabled` | Enable parallel execution | `true` | | `cucumber.execution.parallel.config.strategy` | Execution strategy | `dynamic`, `fixed` | | `cucumber.execution.parallel.config.fixed.parallelism` | Number of parallel threads (fixed) | `4` | | `cucumber.execution.parallel.config.dynamic.factor` | Multiplier for available processors | `1.0` | ## Multiple Test Suites You can create multiple test suites for different purposes: ```java // Smoke tests @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@smoke") class SmokeTestSuite {} // Regression tests @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter(key = FILTER_TAGS_PROPERTY_NAME, value = "@regression") class RegressionTestSuite {} // Specific feature area @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features/authentication") class AuthenticationTestSuite {} ``` ## Best Practices ### 1. Feature File Organization - Group related features in directories - Use meaningful feature names - Keep scenarios focused and independent - Use Background for common setup ### 2. Tagging Strategy - Use tags consistently across the project - Common tags: `@smoke`, `@regression`, `@wip`, `@bug`, `@slow` - Tag by feature area: `@authentication`, `@checkout`, `@search` - Tag by test type: `@ui`, `@api`, `@integration` ### 3. Step Definitions - Write reusable steps - Use Cucumber expressions for flexibility - Keep step implementations thin - delegate to action classes or Screenplay tasks - Avoid duplicating step definitions ### 4. Scenario Design - Write scenarios from the user's perspective - Use Given-When-Then structure consistently - Keep scenarios focused on a single behavior - Use scenario outlines for data-driven tests ### 5. Reporting - Always configure the Serenity reporter - Use meaningful scenario and step names - Add relevant tags for filtering reports - Review reports regularly ## Troubleshooting ### Common Issues **1. JUnit 5 tests not running alongside Cucumber tests** If you have both JUnit 5 tests and Cucumber tests in your project, but only Cucumber tests are being discovered and run, this is likely because Cucumber's `cucumber.features` property causes other discovery selectors to be ignored. You'll see a warning like: ``` WARNING: Discovering tests using the cucumber.features property. Other discovery selectors are ignored! ``` The solution is to configure separate Maven Failsafe executions for JUnit and Cucumber tests. See [Running JUnit 5 and Cucumber Tests Together](/docs/guide/maven#running-junit-5-and-cucumber-tests-together) for the full configuration. **2. "No tests found"** - Ensure `@Suite` and `@IncludeEngines("cucumber")` are present - Check that feature files are in the correct location - Verify step definitions are in the glue path **3. "Step undefined"** - Implement the missing step definition - Check the glue configuration points to your step definitions package - Ensure step regex/expressions match **4. "Plugin not found"** - Verify you're using the correct plugin path: `net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel` - Check Serenity dependencies are included **5. "Tests not running in parallel"** - Ensure parallel configuration is in `junit-platform.properties` - Use `SerenityReporterParallel` (not `SerenityReporter`) - Check JUnit Platform configuration ## Next Steps - Learn about [Parallel Execution](parallel-execution) for faster test runs - Explore [Cucumber Configuration Reference](configuration-reference) for advanced options - Check out the [Screenplay Pattern](/docs/screenplay/screenplay_fundamentals) for better test maintainability - Review [Serenity Reports](/docs/reporting/the_serenity_reports) documentation ## Additional Resources - [Cucumber Documentation](https://cucumber.io/docs/cucumber/) - [JUnit 5 User Guide](https://junit.org/junit5/docs/current/user-guide/) - [Serenity BDD Official Site](https://serenity-bdd.info) - [Example Project](https://github.com/serenity-bdd/serenity-cucumber-starter) --- # Running Cucumber Scenarios in Parallel Running Cucumber scenarios in parallel can dramatically reduce test execution time. This guide explains how to configure and optimize parallel execution with Serenity BDD and JUnit 5. ## Overview Parallel execution in Cucumber with JUnit 5 is powered by the Cucumber JUnit Platform Engine, which integrates with JUnit 5's parallel test execution capabilities. When properly configured, multiple scenarios can run simultaneously, taking advantage of multi-core processors. ## Prerequisites - Serenity BDD 3.6.0 or higher - JUnit 5 - Cucumber JUnit Platform Engine - Thread-safe test code ## Basic Configuration ### Step 1: Add Required Dependencies Ensure you have the Cucumber JUnit Platform Engine dependency: ```xml io.cucumber cucumber-junit-platform-engine 7.33.0 test org.junit.platform junit-platform-suite 5.10.0 test ``` ### Step 2: Use the Parallel Reporter :::warning Important You **must** use `SerenityReporterParallel` for parallel execution, not `SerenityReporter`. ::: Update your test suite to use the parallel-safe reporter: ```java import org.junit.platform.suite.api.*; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) class CucumberTestSuite { } ``` ### Step 3: Configure Parallel Execution Create or update `junit-platform.properties` in `src/test/resources`: ```properties # Enable parallel execution cucumber.execution.parallel.enabled=true # Use dynamic strategy (recommended) cucumber.execution.parallel.config.strategy=dynamic # Configure the Serenity reporter cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel ``` ## Execution Strategies ### Dynamic Strategy (Recommended) The dynamic strategy automatically determines the number of threads based on available processors: ```properties cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1.0 ``` The `factor` is a multiplier for the number of available processors: - `1.0` = same number of threads as processors (recommended) - `0.5` = half the number of processors - `2.0` = double the number of processors **Example:** On a machine with 8 cores: - Factor `1.0` = 8 threads - Factor `0.5` = 4 threads - Factor `2.0` = 16 threads ### Fixed Strategy The fixed strategy uses a specified number of threads: ```properties cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 cucumber.execution.parallel.config.fixed.max-pool-size=4 ``` - `parallelism` - Number of concurrent threads - `max-pool-size` - Maximum size of the thread pool **Use when:** - You need consistent, predictable thread counts - Running on shared CI/CD infrastructure - Debugging parallel execution issues ### Custom Strategy For advanced use cases, you can implement a custom strategy: ```properties cucumber.execution.parallel.config.strategy=custom cucumber.execution.parallel.config.custom.class=com.example.MyParallelStrategy ``` ## Complete Configuration Example **junit-platform.properties:** ```properties # Parallel execution cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1.0 # Serenity reporter cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel # Optional: Control execution order cucumber.execution.order=random # Optional: Fail fast cucumber.execution.fail-fast=false ``` ## Thread Safety Considerations ### WebDriver Management Serenity's `@Managed` WebDriver is automatically thread-safe when using parallel execution: ```java @ExtendWith(SerenityJUnit5Extension.class) class WebDriverSteps { @Managed WebDriver driver; // Thread-safe - each thread gets its own instance @Given("I am on the home page") public void navigateToHomePage() { driver.get("https://example.com"); } } ``` Each thread will have its own isolated WebDriver instance. ### Step Definition Thread Safety Step definition classes must be thread-safe: **✅ Thread-Safe (Recommended):** ```java public class LoginSteps { @Given("{actor} is on the login page") public void navigateToLoginPage(Actor actor) { actor.attemptsTo( Navigate.to("/login") ); } } ``` **❌ Not Thread-Safe:** ```java public class LoginSteps { private String username; // Shared state - NOT thread-safe! @Given("user enters username {string}") public void enterUsername(String user) { this.username = user; // Race condition! } } ``` ### Using Scenario Context For sharing data within a scenario, use Cucumber's scenario context: ```java public class CheckoutSteps { private final ScenarioContext context; public CheckoutSteps(ScenarioContext context) { this.context = context; // Injected per scenario } @When("I add item to cart") public void addItem() { context.put("cartTotal", calculateTotal()); } @Then("cart total should be correct") public void verifyTotal() { Double total = context.get("cartTotal", Double.class); // Verify total } } ``` ## Controlling Parallelism ### Parallel at Different Levels You can control what runs in parallel: #### Scenario-Level Parallelism (Default) ```properties # Each scenario runs in its own thread cucumber.execution.parallel.enabled=true ``` #### Feature-Level Parallelism To run features in parallel but scenarios within a feature sequentially, you need to configure JUnit Platform: **junit-platform.properties:** ```properties junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.mode.classes.default=concurrent ``` ### Limiting Concurrent Scenarios Control the maximum number of concurrent scenarios: ```properties cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 ``` ## Parallel Execution with Tags You can run specific tags in parallel: ```java @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "@parallel" ) class ParallelTestSuite { } ``` Or exclude scenarios from parallel execution: ```properties cucumber.filter.tags=not @serial ``` ## Maven/Gradle Configuration ### Maven Surefire Plugin Configure Maven to support parallel execution: ```xml org.apache.maven.plugins maven-surefire-plugin 3.0.0 true org.apache.maven.plugins maven-failsafe-plugin 3.0.0 **/*TestSuite.java ${webdriver.driver} integration-test verify ``` ### Gradle Configuration For Gradle: ```groovy test { useJUnitPlatform() maxParallelForks = Runtime.runtime.availableProcessors() systemProperty 'cucumber.execution.parallel.enabled', 'true' systemProperty 'cucumber.execution.parallel.config.strategy', 'dynamic' } ``` ## Performance Optimization ### 1. Choose the Right Strategy - **Small test suites** (<50 scenarios): Fixed strategy with 2-4 threads - **Medium test suites** (50-200 scenarios): Dynamic strategy with factor 1.0 - **Large test suites** (>200 scenarios): Dynamic strategy with factor 0.75-1.0 ### 2. Optimize Scenario Design - **Keep scenarios independent** - No shared state between scenarios - **Minimize setup time** - Use efficient Before hooks - **Avoid sleeps** - Use explicit waits instead - **Clean up resources** - Use After hooks properly ### 3. Resource Management ```properties # Control thread pool size to avoid resource exhaustion cucumber.execution.parallel.config.fixed.max-pool-size=8 # Avoid too many concurrent browsers webdriver.pool.max=10 ``` ### 4. Monitor Resource Usage Watch for: - CPU utilization (should be near 100% for optimal performance) - Memory consumption (each browser instance uses RAM) - Network bandwidth (for remote WebDriver) - Database connections (if applicable) ## Debugging Parallel Tests ### Disable Parallel Execution Temporarily ```properties cucumber.execution.parallel.enabled=false ``` Or via command line: ```bash mvn clean verify -Dcucumber.execution.parallel.enabled=false ``` ### Run with Fewer Threads ```properties cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=1 ``` ### Verbose Logging Enable detailed logging to diagnose issues: ```properties # Log file location cucumber.plugin=pretty,html:target/cucumber-reports.html,json:target/cucumber.json # Serenity logging serenity.verbose=true serenity.logging=VERBOSE ``` ## Common Issues and Solutions ### Issue 1: Flaky Tests **Symptoms:** Tests pass when run individually but fail when run in parallel **Solutions:** - Check for shared state in step definitions - Ensure WebDriver instances are properly isolated - Review test data - avoid shared test data - Add proper synchronization/waits ### Issue 2: Resource Exhaustion **Symptoms:** Tests slow down or crash after some time **Solutions:** - Reduce parallelism: lower the thread count - Increase heap size: `-Xmx4g` - Check for resource leaks (unclosed connections, browser instances) - Monitor system resources ### Issue 3: Report Generation Issues **Symptoms:** Serenity reports are incomplete or corrupted **Solutions:** - Ensure you're using `SerenityReporterParallel` - Check Serenity version (3.6.0+) - Verify maven-failsafe-plugin is configured correctly - Check for filesystem race conditions ### Issue 4: CI/CD Pipeline Failures **Symptoms:** Tests fail only in CI but pass locally **Solutions:** - Match thread count to CI environment capabilities - Use fixed strategy for consistent behavior - Increase timeouts for slower CI environments - Check CI resource limits ## Best Practices ### 1. Start Small Begin with 2-4 threads and gradually increase: ```properties cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=2 ``` ### 2. Use Tags Strategically Mark scenarios that can't run in parallel: ```gherkin @serial @database-migration Scenario: Migrate database schema # This scenario must run alone ``` Then exclude them: ```properties cucumber.filter.tags=not @serial ``` ### 3. Isolate Test Data Each scenario should create and clean up its own test data: ```java @Before public void setupTestData() { // Create unique test data for this scenario String uniqueId = UUID.randomUUID().toString(); context.put("testUserId", "user_" + uniqueId); } @After public void cleanupTestData() { // Clean up this scenario's test data String userId = context.get("testUserId", String.class); deleteUser(userId); } ``` ### 4. Monitor and Adjust - Monitor test execution times - Track flaky test rates - Adjust parallelism based on metrics - Review resource utilization ### 5. Document Thread Safety Add comments for non-thread-safe scenarios: ```gherkin # @serial - Modifies global application state @serial @admin Scenario: Update application settings Given I am logged in as admin When I change the default language to Spanish Then all users should see Spanish interface ``` ## Measuring Performance Improvements ### Before Parallel Execution ``` Total scenarios: 100 Execution time: 50 minutes ``` ### After Parallel Execution (8 threads) ``` Total scenarios: 100 Execution time: 8 minutes Speedup: 6.25x Efficiency: 78% ``` Calculate your speedup: ``` Speedup = Sequential Time / Parallel Time Efficiency = Speedup / Number of Threads ``` ## Next Steps - Review the main [Cucumber with JUnit 5](cucumber-junit5) guide - Check the [Configuration Reference](configuration-reference) for all available options - See [Best Practices](cucumber-junit5#best-practices) for writing maintainable tests - Explore [Serenity Reports](/docs/reporting/the_serenity_reports) documentation ## Additional Resources - [Cucumber Parallel Execution Docs](https://github.com/cucumber/cucumber-jvm/tree/main/cucumber-junit-platform-engine#parallel-execution) - [JUnit 5 Parallel Execution](https://junit.org/junit5/docs/current/user-guide/#writing-tests-parallel-execution) - [Serenity BDD Performance Tips](https://serenity-bdd.info) --- # Advanced Cucumber Patterns with Serenity BDD This guide covers advanced patterns and techniques for writing maintainable, scalable Cucumber tests with Serenity BDD. You'll learn patterns used by experienced test automation engineers to build robust test suites. ## Prerequisites This guide assumes you're familiar with: - Basic Cucumber concepts (features, scenarios, step definitions) - Serenity BDD fundamentals - Either the Screenplay pattern or Page Objects If you're new to Cucumber with Serenity, start with [Running Cucumber Scenarios with JUnit 5](cucumber-junit5). ## Advanced Hook Patterns Cucumber hooks allow you to run code at specific points in the test lifecycle. Understanding advanced hook patterns is essential for proper test setup and cleanup. ### Hook Types and Execution Order Cucumber provides several hook types that execute at different points: ```java package com.example.hooks; import io.cucumber.java.*; public class LifecycleHooks { // Runs once before ALL scenarios (Cucumber 7+) @BeforeAll public static void beforeAllScenarios() { System.out.println("Starting test suite"); // Initialize shared resources, start test containers, etc. } // Runs before EACH scenario @Before public void beforeScenario(Scenario scenario) { System.out.println("Starting: " + scenario.getName()); } // Runs before EACH step @BeforeStep public void beforeStep() { // Useful for logging or timing } // Runs after EACH step @AfterStep public void afterStep() { // Useful for screenshots or state verification } // Runs after EACH scenario @After public void afterScenario(Scenario scenario) { System.out.println("Finished: " + scenario.getName()); if (scenario.isFailed()) { // Custom failure handling } } // Runs once after ALL scenarios (Cucumber 7+) @AfterAll public static void afterAllScenarios() { System.out.println("Test suite complete"); // Cleanup shared resources } } ``` **Execution order for a single scenario:** ``` @BeforeAll (once per test run) └─> @Before └─> @BeforeStep → Step 1 → @AfterStep └─> @BeforeStep → Step 2 → @AfterStep └─> @BeforeStep → Step 3 → @AfterStep @After @AfterAll (once per test run) ``` ### Tagged Hooks Run hooks only for scenarios with specific tags: ```java public class TaggedHooks { @Before("@database") public void setupDatabase() { // Only runs for scenarios tagged @database DatabaseTestUtils.resetDatabase(); DatabaseTestUtils.loadFixtures("test-data.sql"); } @After("@database") public void cleanupDatabase() { DatabaseTestUtils.truncateAllTables(); } @Before("@authenticated") public void ensureAuthenticated() { // Setup authenticated session before scenarios that need it } @Before("@slow") public void increaseTimeouts() { // Increase timeouts for slow-running scenarios } } ``` ### Tag Expressions in Hooks Use complex tag expressions to target specific scenarios: ```java public class ConditionalHooks { @Before("@ui and not @headless") public void setupBrowserWithUI() { // Only for UI tests that need a visible browser } @Before("@api or @integration") public void setupApiClient() { // For API or integration tests } @Before("(@smoke or @regression) and not @wip") public void setupForRealTests() { // For smoke or regression, but skip work-in-progress } } ``` ### Hook Ordering Control the order of hook execution when you have multiple hooks of the same type: ```java public class OrderedHooks { // Lower order values run first for @Before hooks @Before(order = 1) public void firstSetup() { System.out.println("1. Initialize test environment"); } @Before(order = 2) public void secondSetup() { System.out.println("2. Setup test data"); } @Before(order = 3) public void thirdSetup() { System.out.println("3. Navigate to application"); } // Higher order values run first for @After hooks (reverse order) @After(order = 3) public void firstCleanup() { System.out.println("1. Capture final state"); } @After(order = 2) public void secondCleanup() { System.out.println("2. Cleanup test data"); } @After(order = 1) public void thirdCleanup() { System.out.println("3. Close resources"); } } ``` :::tip Hook Order Memory Aid - **@Before**: Lower numbers run first (1, 2, 3...) - **@After**: Higher numbers run first (...3, 2, 1) Think of it as wrapping: setup builds up, teardown unwinds in reverse. ::: ### Screenplay-Specific Hooks When using the Screenplay pattern, set up actors in hooks: ```java package com.example.hooks; import io.cucumber.java.*; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.actors.OnStage; import net.serenitybdd.screenplay.actors.OnlineCast; import net.serenitybdd.screenplay.abilities.BrowseTheWeb; public class ScreenplayHooks { @Before public void setTheStage() { OnStage.setTheStage(new OnlineCast()); } @After public void drawTheCurtain() { OnStage.drawTheCurtain(); } } ``` For Playwright-based tests: ```java import net.serenitybdd.screenplay.playwright.abilities.BrowseTheWebWithPlaywright; public class PlaywrightHooks { @Before public void setTheStage() { OnStage.setTheStage(Cast.whereEveryoneCan( BrowseTheWebWithPlaywright.usingTheDefaultConfiguration() )); } } ``` ## Custom Parameter Types Cucumber expressions support custom parameter types that transform step arguments into domain objects. ### Defining Custom Parameter Types ```java package com.example.parameters; import io.cucumber.java.ParameterType; import java.time.LocalDate; import java.time.format.DateTimeFormatter; public class CustomParameters { // Transform "today", "tomorrow", "yesterday" into LocalDate @ParameterType("today|tomorrow|yesterday|\\d{4}-\\d{2}-\\d{2}") public LocalDate date(String value) { return switch (value) { case "today" -> LocalDate.now(); case "tomorrow" -> LocalDate.now().plusDays(1); case "yesterday" -> LocalDate.now().minusDays(1); default -> LocalDate.parse(value); }; } // Transform currency amounts like "$100.50" or "€50" @ParameterType("\\$[\\d,]+\\.?\\d*|€[\\d,]+\\.?\\d*|£[\\d,]+\\.?\\d*") public Money money(String value) { String currency = value.substring(0, 1); double amount = Double.parseDouble(value.substring(1).replace(",", "")); return new Money(amount, currency); } // Transform user roles @ParameterType("admin|manager|user|guest") public UserRole role(String value) { return UserRole.valueOf(value.toUpperCase()); } } ``` ### Using Custom Parameters in Steps ```gherkin Feature: Order Management Scenario: Schedule order for delivery Given an order placed on 2024-01-15 When I schedule delivery for tomorrow Then the delivery date should be after today Scenario: Apply discount to order Given an order totaling $150.00 When a manager applies a 10% discount Then the new total should be $135.00 ``` ```java public class OrderSteps { @Given("an order placed on {date}") public void anOrderPlacedOn(LocalDate orderDate) { // orderDate is already a LocalDate object theActorInTheSpotlight().remember("orderDate", orderDate); } @When("I schedule delivery for {date}") public void scheduleDeliveryFor(LocalDate deliveryDate) { theActorInTheSpotlight().attemptsTo( ScheduleDelivery.forDate(deliveryDate) ); } @Given("an order totaling {money}") public void anOrderTotaling(Money total) { // total is a Money object with amount and currency theActorInTheSpotlight().attemptsTo( CreateOrder.withTotal(total) ); } @When("a {role} applies a {int}% discount") public void applyDiscount(UserRole role, int percentage) { // role is a UserRole enum theActorInTheSpotlight().attemptsTo( ApplyDiscount.of(percentage).asA(role) ); } } ``` ### Actor Parameter Type Serenity BDD provides a built-in `{actor}` parameter type: ```gherkin Scenario: Multiple users interact Given Alice has items in her cart And Bob is browsing the catalog When Alice proceeds to checkout And Bob adds a laptop to his cart Then Alice should see the payment page And Bob should have 1 item in his cart ``` ```java public class MultiActorSteps { @Given("{actor} has items in her/his cart") public void hasItemsInCart(Actor actor) { actor.attemptsTo( AddToCart.item("Sample Product") ); } @When("{actor} proceeds to checkout") public void proceedsToCheckout(Actor actor) { actor.attemptsTo( NavigateTo.theCheckoutPage() ); } @Then("{actor} should have {int} item(s) in her/his cart") public void shouldHaveItemsInCart(Actor actor, int count) { actor.should( seeThat(TheCart.itemCount(), equalTo(count)) ); } } ``` ## Complex Data Table Patterns Data tables are powerful for passing structured data to steps. ### Basic Data Table Transformations ```gherkin Scenario: Create multiple products Given the following products exist: | name | price | category | inStock | | Laptop | 999.99 | Electronics | true | | Headphones | 149.99 | Electronics | true | | Desk Chair | 299.99 | Furniture | false | ``` **Using DataTable directly:** ```java @Given("the following products exist:") public void createProducts(DataTable dataTable) { List> rows = dataTable.asMaps(); for (Map row : rows) { String name = row.get("name"); double price = Double.parseDouble(row.get("price")); String category = row.get("category"); boolean inStock = Boolean.parseBoolean(row.get("inStock")); // Create product... } } ``` **Automatic transformation to POJOs:** ```java public class Product { private String name; private double price; private String category; private boolean inStock; // Getters and setters... } @Given("the following products exist:") public void createProducts(List products) { // Cucumber automatically transforms rows to Product objects for (Product product : products) { theActorInTheSpotlight().attemptsTo( CreateProduct.withDetails(product) ); } } ``` ### DataTableType for Custom Transformations ```java package com.example.parameters; import io.cucumber.java.DataTableType; import java.util.Map; public class DataTableTypes { @DataTableType public Product productEntry(Map entry) { Product product = new Product(); product.setName(entry.get("name")); product.setPrice(parsePrice(entry.get("price"))); product.setCategory(entry.get("category")); product.setInStock("yes".equalsIgnoreCase(entry.get("available"))); // Handle optional fields if (entry.containsKey("sku")) { product.setSku(entry.get("sku")); } return product; } @DataTableType public Address addressEntry(Map entry) { return Address.builder() .street(entry.get("street")) .city(entry.get("city")) .state(entry.get("state")) .zipCode(entry.get("zip")) .country(entry.getOrDefault("country", "USA")) .build(); } private double parsePrice(String price) { return Double.parseDouble(price.replace("$", "").replace(",", "")); } } ``` ### Vertical Data Tables For single-entity data, use vertical tables: ```gherkin Scenario: Create a detailed user profile Given a user with the following details: | First Name | John | | Last Name | Smith | | Email | john@example.com | | Phone | +1-555-123-4567 | | Date of Birth | 1990-05-15 | | Role | Premium Member | ``` ```java @Given("a user with the following details:") public void createUser(DataTable dataTable) { Map userData = dataTable.asMap(); UserProfile profile = UserProfile.builder() .firstName(userData.get("First Name")) .lastName(userData.get("Last Name")) .email(userData.get("Email")) .phone(userData.get("Phone")) .dateOfBirth(LocalDate.parse(userData.get("Date of Birth"))) .role(userData.get("Role")) .build(); theActorInTheSpotlight().attemptsTo( CreateUserProfile.with(profile) ); } ``` ### Nested Data Structures For complex data, combine tables with scenario outlines: ```gherkin Scenario Outline: Validate order with multiple items Given a customer places an order with: | product | quantity | unit price | | | | | | | | | When the order is processed Then the order total should be Examples: | item1 | qty1 | price1 | item2 | qty2 | price2 | total | | Laptop | 1 | $999 | Mouse | 2 | $25 | $1049 | | Monitor | 2 | $300 | Keyboard | 1 | $75 | $675 | ``` ## Doc Strings for Long-Form Content Use doc strings for multi-line text content: ```gherkin Scenario: Submit a support ticket Given the customer has a billing issue When they submit a support ticket with: """ Subject: Incorrect charge on my account Hello, I noticed an incorrect charge of $49.99 on my account dated January 15th. I did not authorize this transaction. Please investigate and refund the amount. Thank you, John Smith Account: JS-12345 """ Then a ticket should be created with priority "High" ``` ```java @When("they submit a support ticket with:") public void submitTicket(String ticketContent) { theActorInTheSpotlight().attemptsTo( SubmitSupportTicket.withContent(ticketContent) ); } ``` ### Doc Strings with Content Types Specify the content type for structured data: ```gherkin Scenario: Create product via API When I send a POST request to "/api/products" with: """json { "name": "Wireless Keyboard", "price": 79.99, "category": "Electronics", "specifications": { "connection": "Bluetooth 5.0", "battery": "AAA x 2", "warranty": "2 years" } } """ Then the response status should be 201 ``` ```java @When("I send a POST request to {string} with:") public void sendPostRequest(String endpoint, String jsonBody) { theActorInTheSpotlight().attemptsTo( Post.to(endpoint).with(request -> request .contentType(ContentType.JSON) .body(jsonBody) ) ); } ``` ## Sharing State Between Steps Managing state across steps is crucial for maintainable tests. ### Using Actor Memory (Screenplay Pattern) The recommended approach with Screenplay: ```java public class OrderSteps { @Given("a customer creates an order") public void createOrder() { String orderId = UUID.randomUUID().toString(); theActorInTheSpotlight().attemptsTo( CreateOrder.withId(orderId) ); // Store for later steps theActorInTheSpotlight().remember("orderId", orderId); } @When("the customer adds {string} to the order") public void addItem(String item) { String orderId = theActorInTheSpotlight().recall("orderId"); theActorInTheSpotlight().attemptsTo( AddToOrder.item(item).forOrder(orderId) ); } @Then("the order should contain {int} items") public void verifyItemCount(int expectedCount) { String orderId = theActorInTheSpotlight().recall("orderId"); theActorInTheSpotlight().should( seeThat(TheOrder.withId(orderId).itemCount(), equalTo(expectedCount)) ); } } ``` ### Typed Memory with Generics Create type-safe memory keys: ```java public class MemoryKeys { public static final String ORDER_ID = "orderId"; public static final String CUSTOMER = "customer"; public static final String CART_ITEMS = "cartItems"; } // Usage actor.remember(MemoryKeys.ORDER_ID, orderId); String id = actor.recall(MemoryKeys.ORDER_ID); ``` ### Using World Objects (Dependency Injection) For complex state management, use Cucumber's dependency injection: **With PicoContainer (default):** ```java // Shared state class public class TestContext { private Order currentOrder; private Customer currentCustomer; private List capturedErrors = new ArrayList<>(); public Order getCurrentOrder() { return currentOrder; } public void setCurrentOrder(Order order) { this.currentOrder = order; } public Customer getCurrentCustomer() { return currentCustomer; } public void setCurrentCustomer(Customer customer) { this.currentCustomer = customer; } public void addError(String error) { capturedErrors.add(error); } public List getErrors() { return capturedErrors; } } // Step definitions receive TestContext via constructor injection public class OrderSteps { private final TestContext context; public OrderSteps(TestContext context) { this.context = context; } @Given("a customer creates an order") public void createOrder() { Order order = new Order(); context.setCurrentOrder(order); } @When("an error occurs: {string}") public void captureError(String error) { context.addError(error); } } ``` **Add PicoContainer dependency:** ```xml io.cucumber cucumber-picocontainer ${cucumber.version} test ``` ## Error Handling Patterns Handle errors gracefully in step definitions. ### Expected Errors Pattern ```gherkin Scenario: Prevent checkout with empty cart Given a customer with an empty cart When they attempt to checkout Then they should see an error: "Your cart is empty" ``` ```java public class ErrorSteps { @When("they attempt to checkout") public void attemptCheckout() { try { theActorInTheSpotlight().attemptsTo( ProceedToCheckout.now() ); // Store that no error occurred theActorInTheSpotlight().remember("checkoutError", null); } catch (CheckoutException e) { // Store the error for verification theActorInTheSpotlight().remember("checkoutError", e.getMessage()); } } @Then("they should see an error: {string}") public void shouldSeeError(String expectedError) { String actualError = theActorInTheSpotlight().recall("checkoutError"); assertThat(actualError).isEqualTo(expectedError); } } ``` ### Using Questions for Error States A cleaner approach with Screenplay: ```java public class ErrorMessage implements Question { public static Question displayed() { return new ErrorMessage(); } @Override public String answeredBy(Actor actor) { return Text.of(".error-message").answeredBy(actor); } } // In steps @Then("they should see an error: {string}") public void shouldSeeError(String expectedError) { theActorInTheSpotlight().should( seeThat(ErrorMessage.displayed(), equalTo(expectedError)) ); } ``` ### Soft Assertions for Multiple Checks ```java @Then("the order summary should show:") public void verifyOrderSummary(DataTable expected) { Actor actor = theActorInTheSpotlight(); actor.attemptsTo( Ensure.that(OrderSummary.subtotal()).isEqualTo(expected.get("subtotal")), Ensure.that(OrderSummary.tax()).isEqualTo(expected.get("tax")), Ensure.that(OrderSummary.shipping()).isEqualTo(expected.get("shipping")), Ensure.that(OrderSummary.total()).isEqualTo(expected.get("total")) ); } ``` ## Step Definition Organization ### One Class Per Feature Area Organize step definitions by domain area: ``` src/test/java/com/example/stepdefinitions/ ├── authentication/ │ ├── LoginSteps.java │ ├── RegistrationSteps.java │ └── PasswordResetSteps.java ├── shopping/ │ ├── CartSteps.java │ ├── CheckoutSteps.java │ └── PaymentSteps.java ├── common/ │ ├── NavigationSteps.java │ └── VerificationSteps.java └── hooks/ ├── ScreenplayHooks.java └── DatabaseHooks.java ``` ### Thin Steps, Rich Tasks Keep step definitions thin by delegating to Screenplay tasks: ```java // Bad: Logic in step definition @When("the customer adds {string} to their cart") public void addToCart(String productName) { WebDriver driver = getDriver(); driver.findElement(By.cssSelector(".search-input")).sendKeys(productName); driver.findElement(By.cssSelector(".search-button")).click(); WebDriverWait wait = new WebDriverWait(driver, Duration.ofSeconds(10)); wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".product-card"))); driver.findElement(By.cssSelector(".add-to-cart-button")).click(); wait.until(ExpectedConditions.visibilityOfElementLocated(By.cssSelector(".cart-updated"))); } // Good: Delegate to Task @When("the customer adds {string} to their cart") public void addToCart(String productName) { theActorInTheSpotlight().attemptsTo( AddToCart.theProduct(productName) ); } ``` ### Reusable Step Patterns Create generic steps that work across features: ```java public class CommonSteps { @Given("{actor} is on the {word} page") public void navigateToPage(Actor actor, String pageName) { actor.attemptsTo( NavigateTo.thePage(pageName) ); } @Then("{actor} should see the message {string}") public void shouldSeeMessage(Actor actor, String message) { actor.should( seeThat(TheVisibleText.onPage(), containsString(message)) ); } @Then("{actor} should see {int} {word} displayed") public void shouldSeeCount(Actor actor, int count, String elementType) { actor.should( seeThat(TheCount.of(elementType), equalTo(count)) ); } } ``` ## Background Best Practices ### Keep Backgrounds Simple ```gherkin # Good: Simple, essential setup Background: Given the user is logged in as "admin" # Bad: Too much detail in background Background: Given a user with email "admin@example.com" exists And the user has password "SecurePass123!" And the user has role "administrator" And the user's account is active And the user has accepted terms of service When the user navigates to the login page And enters their credentials And clicks the login button Then they should be on the dashboard ``` ### Use Tags to Share Backgrounds ```gherkin @authenticated Feature: Shopping Cart # This feature assumes user is logged in # The @authenticated hook handles login Scenario: Add item to cart When I search for "laptop" And I add the first result to my cart Then my cart should have 1 item ``` ```java @Before("@authenticated") public void loginAsDefaultUser() { OnStage.theActorCalled("Customer").attemptsTo( Login.withCredentials("testuser", "password123") ); } ``` ## Scenario Outline Patterns ### Dynamic Examples with Tags ```gherkin @smoke Scenario Outline: Quick login validation Given I am on the login page When I login as "" Then I should see the "" dashboard @admin Examples: Admin Users | userType | dashboard | | super_admin | Super Admin | | site_admin | Site Admin | @regular Examples: Regular Users | userType | dashboard | | premium | Premium | | standard | Standard | ``` ### Example Tables from External Sources Load examples from files or databases: ```java public class DynamicExamples { @ParameterType(".*\\.csv") public List> csvFile(String filename) { return CsvReader.read("src/test/resources/data/" + filename); } } ``` ## Integration: Screenplay vs Page Objects You can use either pattern with Cucumber, or even mix them. ### Pure Screenplay Approach ```java public class CheckoutSteps { @When("{actor} completes checkout with card ending in {string}") public void completeCheckout(Actor actor, String lastFourDigits) { actor.attemptsTo( CompleteCheckout.withCard(lastFourDigits) ); } @Then("{actor} should receive order confirmation") public void shouldReceiveConfirmation(Actor actor) { actor.should( seeThat(OrderConfirmation.isDisplayed(), is(true)), seeThat(OrderConfirmation.orderNumber(), not(emptyString())) ); } } ``` ### Page Object Approach ```java public class CheckoutSteps { @Steps CheckoutPage checkoutPage; @Steps ConfirmationPage confirmationPage; @When("the user completes checkout with card ending in {string}") public void completeCheckout(String lastFourDigits) { checkoutPage.enterCardEndingIn(lastFourDigits); checkoutPage.submitOrder(); } @Then("the user should receive order confirmation") public void shouldReceiveConfirmation() { assertThat(confirmationPage.isDisplayed()).isTrue(); assertThat(confirmationPage.getOrderNumber()).isNotEmpty(); } } ``` ### Hybrid Approach Use Page Objects for simple pages, Screenplay for complex flows: ```java public class HybridSteps { @Steps LoginPage loginPage; // Simple page, use Page Object @When("the user logs in with valid credentials") public void login() { loginPage.loginAs("user@example.com", "password"); } @When("{actor} completes the multi-step registration") public void completeRegistration(Actor actor) { // Complex flow, use Screenplay actor.attemptsTo( CompleteRegistration.withDefaults() ); } } ``` ## Performance Optimization ### Parallel Execution with Isolated State Ensure your hooks and state management are thread-safe: ```java public class ThreadSafeHooks { // ThreadLocal for parallel execution private static final ThreadLocal contextHolder = new ThreadLocal<>(); @Before public void setUp() { contextHolder.set(new TestContext()); } @After public void tearDown() { contextHolder.remove(); } public static TestContext getContext() { return contextHolder.get(); } } ``` ### Lazy Initialization Don't initialize resources until needed: ```java public class LazyDriverHooks { @Before("@ui") public void initializeDriver() { // Only create browser for UI tests OnStage.setTheStage(new OnlineCast()); } @Before("@api") public void initializeApiClient() { // Only create API client for API tests OnStage.setTheStage(Cast.whereEveryoneCan( CallAnApi.at(baseUrl) )); } } ``` ## Debugging Tips ### Verbose Logging in Hooks ```java @BeforeStep public void logStepStart() { System.out.println(">>> Starting step at " + LocalDateTime.now()); } @AfterStep public void logStepEnd(Scenario scenario) { System.out.println("<<< Step completed, scenario status: " + scenario.getStatus()); } ``` ### Capture State on Failure ```java @After public void captureOnFailure(Scenario scenario) { if (scenario.isFailed()) { // Capture screenshot byte[] screenshot = theActorInTheSpotlight() .usingAbilityTo(BrowseTheWeb.class) .takeScreenshot(); scenario.attach(screenshot, "image/png", "failure-screenshot"); // Capture page source String pageSource = theActorInTheSpotlight() .asksFor(ThePageSource.current()); scenario.attach(pageSource, "text/html", "page-source"); // Capture browser console List consoleLogs = theActorInTheSpotlight() .asksFor(ConsoleMessages.all()); scenario.attach(String.join("\n", consoleLogs), "text/plain", "console-logs"); } } ``` ## Summary These advanced patterns help you build maintainable, scalable Cucumber test suites: | Pattern | When to Use | |---------|-------------| | Tagged Hooks | Conditional setup/teardown based on scenario tags | | Ordered Hooks | Control execution order of multiple hooks | | Custom Parameters | Transform step arguments to domain objects | | Data Table Types | Automatic transformation of table rows | | Actor Memory | Share state between steps (Screenplay) | | World Objects | Complex state with dependency injection | | Thin Steps | Keep steps minimal, delegate to tasks | | Soft Assertions | Verify multiple conditions before failing | ## Next Steps - Explore [Parallel Execution](parallel-execution) for faster test runs - Learn about [Cucumber Configuration](configuration-reference) options - Master the [Screenplay Pattern](/docs/screenplay/screenplay_fundamentals) - Review [Serenity Reporting](/docs/reporting/the_serenity_reports) for rich documentation ## Additional Resources - [Cucumber Expressions Documentation](https://cucumber.io/docs/cucumber/cucumber-expressions/) - [Cucumber Hooks Reference](https://cucumber.io/docs/cucumber/api/#hooks) - [Serenity BDD Screenplay Guide](/docs/screenplay/screenplay_fundamentals) --- # Cucumber Configuration Reference This page provides a comprehensive reference of all configuration options for running Cucumber with Serenity BDD and JUnit 5. ## Configuration Methods There are three ways to configure Cucumber with JUnit 5: 1. **@ConfigurationParameter annotations** (in test suite classes) 2. **junit-platform.properties** file (in src/test/resources) 3. **System properties** (command line or build tool) Priority order: Annotations > System Properties > Properties File ## Essential Serenity Configuration ### Serenity Reporter Plugin **Required for Serenity reports** ```java @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) ``` Or in `junit-platform.properties`: ```properties cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel ``` :::warning Breaking Change in Serenity 5.0.0 The plugin path changed from `io.cucumber.core.plugin.*` to `net.serenitybdd.cucumber.core.plugin.*` ::: **Available reporters:** - `SerenityReporterParallel` - Thread-safe, recommended for all scenarios - `SerenityReporter` - Legacy, not thread-safe ## Core Cucumber Configuration ### Feature File Location Specify where to find feature files: ```java // Annotation approach @SelectClasspathResource("features") // Or using configuration parameter @ConfigurationParameter( key = FEATURES_PROPERTY_NAME, value = "src/test/resources/features" ) ``` **junit-platform.properties:** ```properties cucumber.features=src/test/resources/features ``` ### Step Definitions (Glue) Specify packages containing step definitions: ```java @ConfigurationParameter( key = GLUE_PROPERTY_NAME, value = "com.example.stepdefinitions,com.example.hooks" ) ``` **junit-platform.properties:** ```properties cucumber.glue=com.example.stepdefinitions,com.example.hooks ``` **Multiple packages:** Separate with commas ### Tag Filtering Filter scenarios by tags: ```java @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "@smoke and not @wip" ) ``` **junit-platform.properties:** ```properties cucumber.filter.tags=@smoke and not @wip ``` **Tag expression syntax:** - `@tag1 and @tag2` - Both tags required - `@tag1 or @tag2` - Either tag required - `not @tag` - Exclude tag - `(@tag1 or @tag2) and not @tag3` - Complex expressions **Command line:** ```bash mvn verify -Dcucumber.filter.tags="@smoke" ``` ## Parallel Execution Configuration ### Enable Parallel Execution ```properties cucumber.execution.parallel.enabled=true ``` ### Execution Strategy **Dynamic (recommended):** ```properties cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1.0 ``` Factor calculation: `threads = processors × factor` **Fixed:** ```properties cucumber.execution.parallel.config.strategy=fixed cucumber.execution.parallel.config.fixed.parallelism=4 cucumber.execution.parallel.config.fixed.max-pool-size=4 ``` **Custom:** ```properties cucumber.execution.parallel.config.strategy=custom cucumber.execution.parallel.config.custom.class=com.example.MyStrategy ``` ### Parallel Execution Mode ```properties # Parallel scenarios (default) cucumber.execution.parallel.mode.default=concurrent # Parallel features cucumber.execution.parallel.mode.features.default=concurrent ``` ## Execution Behavior ### Execution Order Control scenario execution order: ```properties # Random order (good for finding test dependencies) cucumber.execution.order=random # Alphabetical order cucumber.execution.order=lexicographical # Reverse alphabetical cucumber.execution.order=reverse ``` ### Dry Run Check for undefined steps without executing: ```java @ConfigurationParameter( key = EXECUTION_DRY_RUN_PROPERTY_NAME, value = "true" ) ``` **junit-platform.properties:** ```properties cucumber.execution.dry-run=true ``` ### Wip (Work In Progress) Run only scenarios tagged with @wip: ```properties cucumber.execution.wip=true ``` ### Strict Mode Treat undefined and pending steps as errors: ```properties cucumber.execution.strict=true ``` ## Plugin Configuration ### Multiple Plugins Configure multiple plugins: ```properties cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel,\ pretty,\ html:target/cucumber-reports/cucumber.html,\ json:target/cucumber-reports/cucumber.json,\ junit:target/cucumber-reports/cucumber.xml ``` ### Common Plugins | Plugin | Purpose | Example | |--------|---------|---------| | `pretty` | Console output with colors | `pretty` | | `html` | HTML report | `html:target/cucumber.html` | | `json` | JSON report | `json:target/cucumber.json` | | `junit` | JUnit XML report | `junit:target/cucumber.xml` | | `timeline` | Timeline report | `timeline:target/timeline` | | `usage` | Step usage report | `usage:target/usage.json` | | `rerun` | Failed scenarios file | `rerun:target/rerun.txt` | ### Suppress Cucumber Messages Suppress "Share your results" message: ```java @ConfigurationParameter( key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true" ) ``` **junit-platform.properties:** ```properties cucumber.publish.quiet=true ``` ## Output Formatting ### ANSI Colors Disable colored console output: ```java @ConfigurationParameter( key = ANSI_COLORS_DISABLED_PROPERTY_NAME, value = "true" ) ``` **junit-platform.properties:** ```properties cucumber.ansi-colors.disabled=true ``` ### Monochrome Output ```properties cucumber.plugin=pretty,monochrome ``` ### Snippet Type Configure generated snippet style: ```properties # Camelcase (recommended for Java) cucumber.snippet-type=camelcase # Underscore cucumber.snippet-type=underscore ``` ## Object Factory ### Custom Object Factory For dependency injection: ```java @ConfigurationParameter( key = OBJECT_FACTORY_PROPERTY_NAME, value = "com.example.CustomObjectFactory" ) ``` **junit-platform.properties:** ```properties cucumber.object-factory=com.example.CustomObjectFactory ``` **Common factories:** - PicoFactory (default) - Spring - Guice - OpenEJB - Weld ## JUnit Platform Integration ### Test Discovery ```properties # Include/exclude test classes junit.jupiter.testclass.include.classname.pattern=.*IT junit.jupiter.testclass.exclude.classname.pattern=.*IntegrationTest # Include/exclude test methods junit.jupiter.testmethod.include.pattern=test.* junit.jupiter.testmethod.exclude.pattern=.*Slow ``` ### Parallel Execution (JUnit Level) ```properties junit.jupiter.execution.parallel.enabled=true junit.jupiter.execution.parallel.mode.default=concurrent junit.jupiter.execution.parallel.mode.classes.default=concurrent junit.jupiter.execution.parallel.config.strategy=dynamic junit.jupiter.execution.parallel.config.dynamic.factor=1.0 ``` ## Complete Configuration Example ### Comprehensive junit-platform.properties ```properties # ========================================== # Serenity BDD Configuration # ========================================== cucumber.plugin=net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel # ========================================== # Feature Files and Step Definitions # ========================================== cucumber.features=src/test/resources/features cucumber.glue=com.example.stepdefinitions,com.example.hooks # ========================================== # Filtering # ========================================== cucumber.filter.tags=not @wip cucumber.filter.name=.*checkout.* # ========================================== # Parallel Execution # ========================================== cucumber.execution.parallel.enabled=true cucumber.execution.parallel.config.strategy=dynamic cucumber.execution.parallel.config.dynamic.factor=1.0 # ========================================== # Execution Behavior # ========================================== cucumber.execution.order=random cucumber.execution.dry-run=false cucumber.execution.strict=true # ========================================== # Plugins and Reporting # ========================================== cucumber.plugin=pretty,\ html:target/cucumber-reports/cucumber.html,\ json:target/cucumber-reports/cucumber.json,\ junit:target/cucumber-reports/cucumber.xml cucumber.publish.quiet=true # ========================================== # Output Formatting # ========================================== cucumber.ansi-colors.disabled=false cucumber.snippet-type=camelcase # ========================================== # JUnit Platform # ========================================== junit.jupiter.execution.parallel.enabled=false junit.jupiter.testinstance.lifecycle.default=per_method ``` ### Complete Test Suite Example ```java import org.junit.platform.suite.api.*; import static io.cucumber.junit.platform.engine.Constants.*; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) @ConfigurationParameter( key = GLUE_PROPERTY_NAME, value = "com.example.stepdefinitions" ) @ConfigurationParameter( key = FILTER_TAGS_PROPERTY_NAME, value = "@smoke and not @wip" ) @ConfigurationParameter( key = PLUGIN_PUBLISH_QUIET_PROPERTY_NAME, value = "true" ) class CucumberTestSuite { } ``` ## Environment-Specific Configuration ### Using Profiles Create multiple properties files: ``` src/test/resources/ ├── junit-platform.properties (default) ├── junit-platform-ci.properties (CI environment) └── junit-platform-dev.properties (development) ``` Activate via Maven: ```bash mvn verify -Dprofile=ci ``` ### Environment Variables Override properties using environment variables: ```bash export CUCUMBER_FILTER_TAGS="@smoke" export CUCUMBER_EXECUTION_PARALLEL_ENABLED=true mvn verify ``` ## Constants Reference All constants from `io.cucumber.junit.platform.engine.Constants`: | Constant | Property Key | |----------|--------------| | `ANSI_COLORS_DISABLED_PROPERTY_NAME` | `cucumber.ansi-colors.disabled` | | `EXECUTION_DRY_RUN_PROPERTY_NAME` | `cucumber.execution.dry-run` | | `EXECUTION_ORDER_PROPERTY_NAME` | `cucumber.execution.order` | | `EXECUTION_STRICT_PROPERTY_NAME` | `cucumber.execution.strict` | | `FEATURES_PROPERTY_NAME` | `cucumber.features` | | `FILTER_NAME_PROPERTY_NAME` | `cucumber.filter.name` | | `FILTER_TAGS_PROPERTY_NAME` | `cucumber.filter.tags` | | `GLUE_PROPERTY_NAME` | `cucumber.glue` | | `OBJECT_FACTORY_PROPERTY_NAME` | `cucumber.object-factory` | | `PLUGIN_PROPERTY_NAME` | `cucumber.plugin` | | `PLUGIN_PUBLISH_ENABLED_PROPERTY_NAME` | `cucumber.publish.enabled` | | `PLUGIN_PUBLISH_QUIET_PROPERTY_NAME` | `cucumber.publish.quiet` | | `PLUGIN_PUBLISH_TOKEN_PROPERTY_NAME` | `cucumber.publish.token` | | `SNIPPET_TYPE_PROPERTY_NAME` | `cucumber.snippet-type` | ## Next Steps - Return to the main [Cucumber with JUnit 5](cucumber-junit5) guide - Learn about [Parallel Execution](parallel-execution) - Review [Best Practices](cucumber-junit5#best-practices) ## Additional Resources - [Cucumber Configuration Docs](https://github.com/cucumber/cucumber-jvm/tree/main/cucumber-junit-platform-engine#configuration-options) - [JUnit Platform Configuration](https://junit.org/junit5/docs/current/user-guide/#running-tests-config-params) --- # Screenplay Fundamentals The Screenplay Pattern is a modern test automation design pattern intented to make it easier to write scalabble, maintainable test code. You can get a quick overview of a simple Screenplay test [here](../../docs/tutorials/screenplay). In this section, we will look at how to write high quality test automation using the Screenplay pattern in more detail. We will intially focus on interacting with a web application using Selenium - in the following sections, we will see how to interact with APIs and databases using the Screenplay model. In Screenplay we model _actors_ who interact with an application in various ways to perform _tasks_ that help them acheive their goals. Actors have _abilities_, such as the ability to interact with a web browser or query a database, that help them perform these tasks. Actors can also answer _questions_ about the state of the system, so that we can check whether a feature is behaving the way we would expect it to. ![](img/screenplay-model.svg) ## Setting up a Screenplay test Serenity BDD Screenplay tests can be written using any framwork that works with Serenity BDD. We will start by discussing how to set up a simple Serenity Screenplay test case using common libraries such as JUnit 4, JUnit 5 and Cucumber. ### Screenplay and JUnit 4 Running a Screenplay test in JUnit 4 and JUnit 5 is no different to any other Serenity BDD test. For JUnit 4, add the `serenity-junit` dependency to your project if it is not already there: ```xml net.serenity-bdd serenity-junit ${serenity.version} test net.serenity-bdd serenity-screenplay ${serenity.version} test net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} test ``` ### Screenplay and JUnit 5 (Recommended) For JUnit 5, add the following dependency to your project: ```xml net.serenity-bdd serenity-junit5 ${serenity.version} test net.serenity-bdd serenity-screenplay ${serenity.version} test net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} test ``` Next, you need to use the `SerenityJUnit5Extension` class, like this: ```java import net.serenitybdd.junit.runners.SerenityJUnit5Extension; import org.junit.jupiter.api.Test; @ExtendWith(SerenityJUnit5Extension.class) class AddNewTodos { @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() {...} } ``` ### Screenplay and JUnit 4 (Deprecated) :::warning JUnit 4 Deprecated JUnit 4 support is deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 (see above). ::: If you're still using JUnit 4, use the `SerenityRunner` class in your test class, like this: ```java import net.serenitybdd.junit.runners.SerenityRunner; import org.junit.Test; @RunWith(SerenityRunner.class) public class AddNewTodos { @Test public void addToEmptyList() {...} } ``` ### Screenplay and Cucumber To run a Screenplay scenario with Cucumber and Serenity, you just need the standard Screenplay dependencies along with the `serenity-cucumber` dependency, as shown below: ```xml net.serenity-bdd serenity-cucumber ${serenity.version} test net.serenity-bdd serenity-screenplay ${serenity.version} test net.serenity-bdd serenity-screenplay-webdriver ${serenity.version} test ``` #### With JUnit 5 (Recommended) For JUnit 5, use the `@Suite` annotation with the Cucumber JUnit Platform Engine: ```java import org.junit.platform.suite.api.*; import static io.cucumber.junit.platform.engine.Constants.PLUGIN_PROPERTY_NAME; @Suite @IncludeEngines("cucumber") @SelectClasspathResource("features") @ConfigurationParameter( key = PLUGIN_PROPERTY_NAME, value = "net.serenitybdd.cucumber.core.plugin.SerenityReporterParallel" ) class TestSuite {} ``` #### With JUnit 4 (Deprecated) :::warning JUnit 4 Deprecated JUnit 4 Cucumber runners are deprecated as of Serenity 5.0.0 and will be removed in Serenity 6.0.0. Please migrate to JUnit 5 (see above). ::: For JUnit 4, you would use: ```java import io.cucumber.junit.CucumberOptions; import net.serenitybdd.cucumber.CucumberWithSerenity; import org.junit.runner.RunWith; @RunWith(CucumberWithSerenity.class) @CucumberOptions( features = "classpath:features" ) public class TestSuite {} ``` ## Defining the actors Every Screenplay test needs at least one actor (and some have several). There are several ways we can define an actor, which vary slightly depending on the test framework you are using. ### Actors in JUnit One way to create a new actor is to use the `Actor.named()` method. For example, to create an actor named Toby, we could write the following: ```java Actor toby = Actor.named("Toby"); ``` However this actor will not be able to do very much until we give him the ability to interact with our application. That's where _abilities_ come into play. The most commonly-needed ability is the ability to browse the web using a web automation library such as Selenium or Playwright. In a Serenity JUnit test, we can use the `@Managed` annotation to manage the WebDriver lifecycle. Once we have a driver available, we can assign it to an actor using the `BrowseTheWeb` class like this: ```java @ExtendWith(SerenityJUnit5Extension.class) class AddNewTodosWithAManagedDriver { @Managed WebDriver driver; @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { Actor toby = Actor.named("Toby") .whoCan(BrowseTheWeb.with(driver)); ... } } ``` ### Defining actors using the CastMember annotation For many scenarios we can simplify this code shown above. If we just need to use an actor who can interact with a website using Selenium, we can use the `@CastMember` annotation. This annotation will configure an actor with a WebDriver instance, and manage the browser lifecycle for us (so no need for the `@Managed` annotation or `WebDriver` variable). You can see an example of this annotation in action here: ```java @ExtendWith(SerenityJUnit5Extension.class) class AddNewTodosWithACastMember { @CastMember Actor toby; @Test @DisplayName("Add a todo item to an empty list") void addToEmptyList() { toby.attemptsTo( Open.browserOn().the(TodoMvcPage.class), Enter.theValue("Buy some milk").into(".new-todo").thenHit(Keys.RETURN) ); Collection items = toby.asksFor(Text.ofEach(".todo-list label")); assertThat(items).containsExactly("Buy some milk"); String title = toby.asksFor(Text.of(By.tagName("h1"))); assertThat(title).isEqualTo("todos"); } } ``` The name of the actor will be derived from the name of the variable. If you need a more descriptive name, you can use the `name` attribute to provide a free text value, which will be used in the reports: ```java @CastMember(name = "Tim the Enchanter") Actor tim; ``` ### Actors in Cucumber Setting up actors in Cucumber is a little more compicated than in JUnit, because we generally refer to them by name in the Cucumber scenarios. Suppose for example we want to automate the following scenario: ```gherkin Scenario: Add items to an empty list Given Toby starts with an empty list When he adds "Buy some milk" to his list Then the todo list should contain the following items: | Buy some milk | ``` #### Using a Managed driver with Cucumber We can use the same approach as we used for JUnit in our Cucumber step definition files, declaring a Webdriver instance using the `@Managed` annotation and assigning it to our actor using the `BrowseTheWeb` ability class: ```java public class TodoStepDefinitions { @Managed WebDriver driver; Actor toby = Actor.named("Toby"); @Before public void setupActor() { toby.can(BrowseTheWeb.with(driver)); } @Given("Toby starts with an empty list") public void stats_with_an_empty_list() { toby.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/") ); } ``` However, the name of the actor ("Toby") is part of the scenario steps. We may want to have other scenarios with other actors, or have a scenario involving more than one actor. #### Casts and Stages We can make our Cucumber step definitions more flexible by introducing the concept of a _Cast_ of actors, and a _stage_ on which they perform. A _Cast_ is a class whose job is to provide actors with a specific set of abilities, when we need them to play a role in our tests. For example, we can use the `OnlineCast` class to provide actors who are equiped with their own Webdriver instances. This way, we don't need to declare the `@Managed` driver, and each actor can have their own browser if we have a scenario with more than one actor. A _stage_ is where our actors perform their roles. A scenario has a single stage, that we can use to identify an actor by name or find the currently active actor. We need to assign a cast to the stage at the start of each scenario; we can do this by calling the `OnStage.setTheStage()` method and passing in a specific cast, like this: ```java OnStage.setTheStage(new OnlineCast()); ``` Once we have set the stage, we can obtain an actor with a given name using the `OnStage.theActorCalled()` method, like this: ```java Actor toby = OnStage.theActorCalled("Toby"); ``` Using this approach, we could implement the first step of our scenario in a way that will work no matter which actor is involved, like this: ```java @Given("{word} starts with an empty list") public void stats_with_an_empty_list(String actorName) { Actor actor = OnStage.theActorCalled(actorName); actor.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/") ); } ``` It is often useful to define a custom Cucumber expression so that we don't have to call the `OnStage.theActorCalled()` method each time: ```java @ParameterType(".*") public Actor actor(String actorName) { return OnStage.theActorCalled(actorName); } @Given("{actor} starts with an empty list") public void stats_with_an_empty_list(Actor actor) { actor.attemptsTo( Open.url("https://todomvc.com/examples/angularjs/#/") ); } ``` This way, our next step can simply use a parameter of type `Actor`, like this: ```java @When("{actor} adds {string} to his list") public void he_adds_to_his_list(Actor actor, String item) { actor.attemptsTo( Enter.theValue(item).into(".new-todo").thenHit(Keys.RETURN) ); } ``` These methods are defined out-of-the-box with the Serenity Cucumber Starter template on Github, so you don't need to add them yourself if you are using this template. The `OnStage` class also recognises pronouns - when an expression such as "he adds 'Walk the dog' to the todo list" or "her shopping cart should be empty", the `theActorCalled()` method will recognise the pronoun and fetch the last active actor with the `theActorInTheSpotlight()` method. By default, the pronouns are: - he - she - they - it - his - her - their - its You can define your own list of pronouns using the `screenplay.pronouns` Serenity property in the `serenity.conf` file, e.g. ```conf screenplay.pronouns=il,elle,ils,elles,son,sa,leur,leurs ``` #### Casts and Abilities In the previous code, we used the `OnlineCast` class to define the cast of actors: ```java OnStage.setTheStage(new OnlineCast()); ``` Casts are very flexible, and we can set them up to produce actors with additional abilities if we need to. For example, imagine we wanted to give our actors the ability to deliver some coffee. To do this, we can create a `Deliver` ability class. To deliver a coffee, we will call the `deliverItem()` method, which will (for our purposes) simply return a string with an item with a unique number (such as "Coffee #1"). The Ability class could look like this: ```java public class Deliver implements Ability { final String item; private int counter = 1; private Deliver(String item) { this.item = item; } public static some(String item) { return new Deliver(item) } public String deliverItem() { return item + " #" + counter++; } } ``` Now we can assign this ability to our actors in the `Cast` by using the `Cast.whereEveryoneCan()` method: ```java OnStage.setTheStage( Cast.whereEveryoneCan(Fetch.some("Coffee")) ); ``` If we needed a more complex setup, we can also use a Lambda expression to provide the code we need to execute to enhance a new actor with additional abilities, like this: ```java Consumer fetchTheCoffee = { actor -> actor.whoCan(Fetch.some("Coffee")) } OnStage.setTheStage( Cast.whereEveryoneCan(fetchTheCoffee) ); ``` #### The Actor in the spotlight Sometimes a step does not refer to an actor by name, but implicitly refers to the last actor who did anything in the scenario. For example, the third step, "Then the todo list should contain the following items..." does not have an explicit actor. In these situations, we can use the `OnStage.theActorInTheSpotlight()` method to refer to the last known active actor. For example, we could implement the third step of our scenario like this: ```java @Then("the todo list should contain the following items:") public void the_todo_list_should_contain(List expectedItems) { Actor currentActor = OnStage.theActorInTheSpotlight(); var todos = currentActor.asksFor(Text.ofEach(".todo-list li")); assertThat(todos).containsExactlyElementsOf(expectedItems); } ``` ## Grouping interactions into tasks Screenplay aims to make it easy to write maintainable and scaleable automation code. And an important way to do this is by grouping interactions into reusable sequences that represent higher level domain concepts. For example, the following test shows how to login to the https://www.saucedemo.com/ test site: ```java toby.attemptsTo( Open.url("https://www.saucedemo.com/"), Enter.theValue("standard_user").into("#user-name"), Enter.theValue("secret_sauce").into("#password"), Click.on("#login-button") ); ``` These four lines involve all login in to the site. However it is hard to see that at a glance when reading the code. We could make this code much more readable by grouping these interactions into a single task. ### Grouping interactions using the Task class A better approach would be to group these related interactions into a single `Task`. The simplest way to do this is to use the `Task.where()` method. This method lets us return a task (or more precisely, an implementation of the `Performable` interface) that combines several other tasks or interactions. The task that allows an actor to login as a standard user could look like this: ```java public class Login { public static Task asAStandardUser() { return Task.where( Open.url("https://www.saucedemo.com/"), Enter.theValue("standard_user").into("#user-name"), Enter.theValue("secret_sauce").into("#password"), Click.on("#login-button") ); } } ``` We might want to make our task more configurable, by adding some parameters to the method signature. In this case, it is useful to make our task more descriptive by adding a text description before the list of _Performable_s, as shown below. This description will appear in the reports whenever the task is executed. The `{0}` in the description will be replaced by the name of the actor (so "Toby" in our test). ```java public class Login { public static Performable as(String username, String password) { return Task.where( "{0} logs in as " + username, Open.url("https://www.saucedemo.com/"), Enter.theValue(username).into("#user-name"), Enter.theValue(password).into("#password"), Click.on("#login-button") ); } } ``` We can now refactor our original test using this method: ```java toby.attemptsTo(Login.asAStandardUser()); ``` or ```java toby.attemptsTo(Login.as("standard_user","secret_sauce")); ``` ### Using lambda expressions to create custom tasks So far we havce seen how to create a _Task_ from a list of other tasks or interactions. But sometimes we need to have more control over the logic of our task. We can do this very easily using lambda expressions, which give us total control over how our task should work. For example, imagine we have refactored our TodoMVC example above to use the following Task class to open the TodoMVC application with an empty list: ```java public class Start { public static Performable withAnEmptyTodoList() { return Task.where("{0} starts with an empty todo list", Open.url("https://todomvc.com/examples/angularjs/#/") ); } ``` We could also create a task to add a todo item to the list: ```java public class AddATodoItem { public static Performable called(String thingToDo) { return Task.where("{0} adds a todo item called " + thingToDo, Enter.theValue(item).into(".new-todo").thenHit(Keys.RETURN) ); } } ``` Now suppose we want to allow Toby to start with some elements already in his list. We could imagine a new `Start.withAListContainingTheItems(...)` method like this: ```java toby.attemptsTo( Start.withAListContaingTheItems("Feed the cat","Buy some bread") ); ``` However to implement this method, we need to loop over the provided list of items, and call the `AddATodoItem` task for each of them. We can do this by using a Lambda expression instead of a list of _Performable_s. The Lambda expression takes an actor as a parameter, and allows us to write arbitrary blocks of code to implement our task. For example, the `withAListContaingTheItems()` method might look like the following: ```java public static Performable withAListContainingTheItems(String... items) { return Task.where("{0} starts with a list containing " + Arrays.asList(items), actor -> { actor.attemptsTo(Start.withAnEmptyTodoList()); for (String item : items) { actor.attemptsTo(AddATodoItem.called(item)); } } ); ``` The full `Start` class would now look like this: ```java public class Start { public static Performable withAnEmptyTodoList() { return Task.where("{0} starts with an empty todo list", Open.url("https://todomvc.com/examples/angularjs/#/") ); } public static Performable withAListContainingTheItems(String... items) { return Task.where("{0} starts with a list containing " + Arrays.asList(items), actor -> { actor.attemptsTo(Start.withAnEmptyTodoList()); for (String item : items) { actor.attemptsTo(AddATodoItem.called(item)); } } ); } } ``` ### Writing custom Performable classes. Sometimes we need more control over the creation of a Task. We can do this by creating a class that implements the `Performable` interface. The `Performable` interface has a single method that you need to implement: `performAs()`. This method takes an `Actor` as a parameter. A very simple custom task might look like this: ```java public class AddItem implements Performable { @Override public void performAs(T actor) { actor.attemptsTo( Enter.theValue("Buy some milk") .into(".new-todo") .thenHit(Keys.RETURN) ); } } ``` Now we can use this class directly in our tests: ```java AddItem addANewItem = new AddItem(); toby.attemptsTo(addANewItem); ``` By default, Serenity will record this action in the reports using the name of the actor and the name of the class, so in this case, the report would include something like "Toby add item". If we want a more descriptive text, we can add the `@Step` annotation to the `performAs()` method, as shown here (the "{0}" placeholder will be replaced by the name of the actor): ```java public class AddItem implements Performable { @Override @Step("{0} adds an item to the list") public void performAs(T actor) { actor.attemptsTo( Enter.theValue("Buy some milk") .into(".new-todo") .thenHit(Keys.RETURN) ); } } ``` In this class, we can only add the item "Buy some milk" to our list. We could make our class more flexible by recordinh the name of the task as a parameter. To do this, we need to add a member variable to our `Performable` to represent this value, and pass it in to the constructor: ```java public class AddItem implements Performable { private String name; public AddItem(String name) { this.name = name; } @Override @Step("{0} adds an item to the list") public void performAs(T actor) { actor.attemptsTo( Enter.theValue(name) .into(".new-todo") .thenHit(Keys.RETURN) ); } public static AddItem called(String name) { return new AddItem(name); } } ``` We can now make our test code more readable, as shown here: ```java toby.attemptsTo(AddItem.named("Walk the dog")); ``` However there is one more thing we need to do to make this work correctly. The code shown here will not be correctly reported in the Serenity reports. For technical reasons, every `Performable` implementation must have a default constructor, even if you don't use it directly in your code. So the complete implementation will look like this: ```java public class AddItem implements Performable { private String name; public AddItem() {} public AddItem(String name) { this.name = name; } @Override @Step("{0} adds an item to the list") public void performAs(T actor) { actor.attemptsTo( Enter.theValue(name) .into(".new-todo") .thenHit(Keys.RETURN) ); } public static AddItem called(String name) { return new AddItem(name); } } ``` :::tip Always remember to add a default constructor with no parameters to your `Performable` implementations, otherwise they will not appear correctly in the reports. ::: ## Questions - querying the state of the system Screenplay is an actor-centric pattern, and just as actors interact with the system by performing _tasks_ and _interactions_, they can query the state of the system by asking _questions_. ### Using bundled Question classes An actor can ask a question by using the `askFor()` method. You can see an example of how this works here: ```java String pageTitle = toby.asksFor(Text.of(".title")); ``` `Text` is an example of a _Question Factory_. Question Factories provide a convenient way to query the state of the application, and there are many to choose from. For example, we could check whether a particular web element was visible using the `Visibility` class, like this: ```java boolean isVisible = toby.asksFor(Visibility.of(".title")); ``` Serenity BDD comes with lot bundled _Question Factories_ like these, and we will look at them in more detail in the next section. But you can also write your own custom questions, to query any aspect of your application that you like. ### Creating custom Questions using Lambda expressions One way to implement a _Question_ is to use a Java 8 Lambda expression that takes an _Actor_ as an argument. For example, the following method returns an _Integer Question_ (a _Question_ that returns an integer). ```java Question numberOfTodoItems() { return actor -> BrowseTheWeb.as(actor).findAll(".todo-list li").size(); } ``` We could find the answer to this question using the `asksFor()` method, as in the previous example: ```java int numberOfTodos = toby.asksFor(numberOfTodoItems()); ``` An alternative to the `asksFor()` method is the `Question`'s own `answeredBy()` method: ```java int numberOfTodos = numberOfTodoItems().answeredBy(toby); ``` This format can be more convenient for assertions, as illustrated in this example: ```java assertThat(numberOfTodoItems().answeredBy(toby)).isEqualTo(1); ``` ### Making questions more readable with the `Question.about()` method We could make this method more readable by using the `Question.about` format - this will produce a more meaningful error message. In the following example, we use the `BrowseTheWeb` class to access the Serenity WebDriver API and find the number of todo list items currently on the page: ```java Question numberOfTodoItems() { return Question.about("the number of todo items") .answeredBy( actor -> BrowseTheWeb.as(actor).findAll(".todo-list li").size()); } ``` ### Converting Questions Questions also provide a convenient way to convert responses into different types. This is particularly useful for the bundled WebDriver `Question` classes, that typically return values in text form. For example, the TodoMVC application shows the number of items remaining to do at any time beneath the list: ![](img/items-left.png) We can locate the item count using the CSS expression _".todo-count strong"_. We could fetch this value as a String using the `Text` class we saw previously: ```java String itemsLeftCount = toby.asksFor(Text.of(".todo-count strong"); ``` However if we prefer to retrieve the value as an integer, we could use the `asInteger()` method like this: ```java int itemsLeftCount = toby.asksFor(Text.of(".todo-count strong").asInteger()); ``` Some of the conversion methods available include the following: | Type | Method | Example | | ------------ | ----------- | ----------- | | Integer | asInteger() | Text.of(".todo-count strong").asInteger() | | Long | asLong() | Text.of(".todo-count strong").asLong() | | Float | asFloat() | Text.of(".currency-value").asFloat() | | Double | asDouble() | Text.of(".currency-value").asDouble() | | BigDecimal | asBigDecimal() | Text.of(".currency-value").asBigDecimal() | ### Converting date values Date values can also be converted to `LocalDate` objects. If the date uses the standard ISO date format (e.g. "2022-05-15"), it can be converted using the `asDate()` method, like this: ```java LocalDate dateValue = toby.asksFor( Text.of("#departure-date").asDate() ); ``` If the date uses a different format, the `asDate()` method takes a date-time pattern that can be used to parse the value, e.g. ```java LocalDate dateValue = toby.asksFor( Text.of("#departure-date").asDate("d MMM uuuu") ); ``` ### Converting to lists of values We can also use the `asListOf()` method to find all the answers to a specific question, and convert each of them to a specified type. For example, to convert a list of matching String values to integers, we could use the following code: ```java List itemQuantities = toby.asksFor( Text.of(".item-quantity").asListOf(Integer.class) ); ``` ### Working with collections Another way to retrieve a list of values is to use the `ofEach()` method. ```java Collection itemQuantities = toby.asksFor(Text.of(".item-quantity")); ``` As with the `Text.of()` method, we can use the `asListOf()` method to convert this collection into another type ```java List itemQuantities = toby.asksFor( Text.of(".item-quantity").asListOf(Integer.class) ); ``` If we need to perform a more complex operation, we can use the `mapEach()` method to apply an arbitrary Lambda expression to each matching element: ```java Collection nameLengths = toby.asksFor(Text.ofEach(".name").mapEach(s -> s.length())); ``` ## Actor-specific session data Sometimes we need to store information in a step or task and reuse it in a subsequent one. Each Screenplay actor can remember information, and retrieve it later on in the test. We do this using the `remember()` method: ```java int actualCost = 100 actor.remember("Total Cost", total); ``` Later on, we can retrieve this information using the `recall()` method: ```java int recalledCost = dana.recall("Total Cost"); assertThat(recalledCost).isEqualTo(100); ``` An actor can remember information of any type, even the answer to a question. For example,the `Text.of()` method returns a Question object of type `Question`. An actor can remember the answer to this question by using it as a parameter to the `remember()` method: ```java actor.remember("Total Cost", Text.of("#total-cost").asInteger()); ``` We can then recall this value in the same way as any other: ```java int totalCost = actor.recall("Total Cost"); assertThat(totalCost).isEqualTo(100); ``` We can even recall a map containing every value currently remembered by the actor. We do this using the `recallAll()` method: ```java Map all = actor.recallAll(); ``` We can also remove a remembered value using the `forget()` method: ```java actor.forget("Total Cost"); ``` ## Conclusion Now that we have seen how to set up a Screenplay test using different frameworks, how to organise interactions into tasks, and how to query the state of the system, we will look at how to use Screenplay to interact with a web application in more detail. --- # Web Testing with Serenity Screenplay ## Introduction Web tests are a common use case for Screenplay scenarios, where we try to model user behaviour and interactions with the system. In this section, we will learn how to interact with a web application using the Screenplay WebDriver integration. ## Opening a URL ### Opening a URL directly In Screenplay, you open a new page using the `Open` interaction class. This can work with a URL, e.g: ```java toby.attemptsTo(Open.url("https://todomvc.com/examples/angularjs/#/")); ``` ### Opening the URL of a Page Object If you have defined a Page Object with a default url, you can open a page object by referring to the page object class. Suppose we have defined the following Serenity Page Object, and set the `@DefaultUrl` value to the TodoMVC application URL: ```java @DefaultUrl("https://todomvc.com/examples/angularjs/#/") public class TodoMvcPage extends PageObject {} ``` We can now open this page using the `Open.browserOn()` method, like this: ```java toby.attemptsTo(Open.browserOn().the(TodoMvcPage.class)); ``` ### Using named pages Sometimes it can be convenient to store the URLs for different environments or servers in the `serenity.conf` file, and refer to them by the name of the property in our test code. For example, imagine we wanted to run our tests against the Angular, React and Polymer implementations of the TodoMVC app. Each application has a different URL, that we could store in the `serenity.conf` file like this: ```json pages { angular = "https://todomvc.com/examples/angularjs/#/" react = "https://todomvc.com/examples/react/#/" polymer = "https://todomvc.com/examples/polymer/index.html" } ``` Next, we can refer to these properties in our code using the `thePageNamed()` method, like this: ```java toby.attemptsTo(Open.browserOn().thePageNamed("pages.react")); ``` ## Locating elements on a page In Screenplay, you can use several different strategies to locate the elements you need to interact with. ### CSS and XPath The simplest way to locate an element is to use a CSS or XPath expression, as shown here: ```java toby.attemptsTo( Click.on("#login-button") ); ``` Or ```java toby.attemptsTo( Click.on("//input[@id='login-button']") ); ``` Serenity will interpret the string to determine whether it is an XPath or CSS expression. In some cases there may be some ambiguity, and Serenity will default to XPath. If this is not intended, you can use the "xpath:" or "css:" prefixes to specify which type of locator you mean: ```java toby.attemptsTo( Click.on("css:input[name=login-button]") ); ``` ### Using Selenium locators You can also use any of the standard Selenium locator (`org.openqa.selenium.By`) classes, as shown here: ```java toby.attemptsTo( Click.on(By.id("login-button")) ); ``` ### Using the Target class Using text or `By` locators has the advantage of being concise, but it can lead to poorly readable test reports, especially when complex or non-meaningful XPath or CSS locators are used. In Screenplay, the `Target` class allows us to give a locator strategy a more meaningful name. For example, consider the following code: ```java toby.attemptsTo(Click.on("//button[.='Add']")); ``` In the Serenity reports, this step will be reported as "Toby clicks on //button[.='Add']", which is not very readable. If we represent this button using the `Target` class, we can associate a label such as "Add to cart button", like this: ```java Target ADD_TO_CART = Target.the("Add to cart button").located(By.cssSelector("//button[.='Add']")); toby.attemptsTo(Click.on(ADD_TO_CART)); ``` In the reports, this step will now appear as "Toby clicks on Add to cart button". ### Using dynamic targets You can also include variables into a `Target` locator, to make your locators dynamic. You can include numbered paramaters using "{0}", "{1}", etc, and then use the `of()` method to instantiate the `Target with the value you are interested in. For example, we could create a generic locator for a button containing a given text like this: ```java Target BUTTON_WITH_LABEL = Target.the("{0} button").located(By.cssSelector("//button[.='{0}']")); toby.attemptsTo(Click.on(BUTTON_WITH_LABEL.of('Add'))); ``` We could even use this dynamic target to define other targets with specific values, like this: ```java Target BUTTON_WITH_LABEL = Target.the("{0} button").located(By.cssSelector("//button[.='{0}']")); Target ADD_BUTTON = BUTTON_WITH_LABEL.of('Add'); toby.attemptsTo(Click.on(ADD_BUTTON)); ``` ### Using Page Elements Serenity [Page Elements](../../docs/guide/page_elements) provide a more intuitive and readable way to locate elements on a page, often without needing to use XPath or CSS. With Page Elements, you can identify elements using expressions such as the following - `Click.on(Button.withText("Add to cart"))` - `Enter.theValue(").into(InputField.withPlaceholder("Enter the customer name"))` - `Click.on(PageElement.locatedBy(".item").containingText("Bananas"))` You can learn more about Page Elements [here](../../docs/guide/page_elements). ## Interacting with elements In this section we will see how to interact with elements on a web page using Selenium WebDriver with Serenity Screenplay. ### Screenplay Interaction classes You can find the standard Serenity interaction classes in the `net.serenitybdd.screenplay.actions` package. | Interaction | Purpose | Example | | ----------- | -------- | ----------- | | Clear | Clear an input field | `actor.attemptsTo(Clear.field("#firstname"))` | | Click | Click on an element | `actor.attemptsTo(Click.on("#add-to-cart"))` | | DoubleClick | Double-click on an element using a Selenium Action | `actor.attemptsTo(DoubleClick.on("#add-to-cart"))` | | Enter | Type a value into an input field | `actor.attemptsTo(Enter.theValue("scott").into("#username"))` | | Evaluate | Evaluate a Javascript expression | `actor.attemptsTo(Evaluate.javascript("window.localStorage.clear();")` | | Hit | Press a key | `actor.attemptsTo(Hit.the(Keys.ENTER).into("#searchterms"))` | | JavaScriptClick | Click on an element using Javascript rather than Selenium | `actor.attemptsTo(JavaScriptClick.on("#add-to-cart"))` | | MoveMouse | Move the mouse over a specified element | `actor.attemptsTo(MoveMouse.to("#main-menu"))` | | Open | Open a specific URL or page | `actor.attemptsTo(Open.url("https://www.google.com"))`| | PerformOn | Perform one or more actions on several elements | See below | | RightClick | Right-click on a given element | `actor.attemptsTo(RightClick.on("#menu"))` | | Scroll | Scroll to an element using Javascript | `actor.attemptsTo(Scroll.to("#terms-and-conditions"))` | | SelectFromOptions | Select a value in an HTML dropdown | `actor.attemptsTo(SelectFromOptions.byVisibleText("Red").from("#color"))` | | SendKeys | Enter a value into a field using the Selenium sendKeys() method |`actor.attemptsTo(SendKeys.of("scott").into("#username"))` | | SetCheckbox | Check a checkbox field | `actor.attemptsTo(SetCheckbox.of("#subscribe-to-newsletter").toTrue())` | | Switch | Switch to another window or tab | `actor.attemptsTo(Switch.toNewWindow())` | | Upload | Upload a file using an HTML upload field | `actor.attemptsTo(Upload.theFile(pathToFile)).to("#uploaded-file"))` | | WithDevTools | Perform an action with the Chrome DevTools | See below | The more important interactions are described in more detail in the following sections. ### Clear The `Clear` interaction resets the value of an HTML form element. ```java dina.attemptsTo(Clear.field(By.id("first-name"))); ``` ### Click Click on a button or element. ```java dina.attemptsTo(Click.on("#some-button")); ``` Sometimes an element is not in an interactable state when we first attempt to click on it. For example, it may be disabled, or not yet visible on the page. In these cases, we can ask Serenity to wait for the element to be enabled using the `afterWaitingUntilEnabled()` method: ```java dina.attemptsTo(Click.on("#some-button").afterWaitingUntilEnabled()); ``` If the element is not yet rendered, we can use the `afterWaitingUntilPresent()` method: ```java dina.attemptsTo(Click.on("#some-button").afterWaitingUntilPresent()); ``` In both these cases, Serenity will wait for up to 5 seconds by default for the element to be present or available. You can configure this timeout using the `webdriver.wait.for.timeout` system property (defined in milliseconds). ### DoubleClick Double-click on a button or element, using Selenium Actions. ```java dina.attemptsTo(DoubleClick.on("#some-button")); ``` ### Enter and SendKeys There are two ways to enter a value into a field. `Enter` will enter a value into a field, first waiting until the field is enabled, and then clearing the field of any current values, before entering the specified value. ```java dina.attemptsTo(Enter.theValue("Sarah-Jane").into("#firstName")); ``` `SendKeys` will perform the equivalent of Selenium `sendKeys()`, you can use `Enter.keyValue()` instead of `Enter.theValue()` ```java dina.attemptsTo(SendKeys.of("Sarah-Jane").into("#firstName")); ``` ### Evaluate a Javascript expression The `Evaluate` task executes a JavaScript command in the context of the currently selected frame or window. For example, the following code will clear the local storage in Dina's browser: ```java dina.attemptsTo(Evaluate.javascript("window.localStorage.clear()")); ``` If the script has a return value (i.e. if the script contains a return statement), you can also retrieve the value returned by a Javascript expression. You can do this in two ways. The first is to use the `result()` method to turn the `Evaluate` action into a question. You can see an example here: ```java Long result = (Long) dina.asksFor(Evaluate.javascript("return 1 + 1").result()); assertThat(result).isEqualTo(2); ``` The type of the object returned by WebDriver is as follows: * For an HTML element, this method returns a `WebElement` * For a decimal, a `Double` is returned * For a non-decimal number, a `Long` is returned * For a boolean, a `Boolean` is returned * For all other cases, a `String` is returned. * For an array, return a `List` with each object following the rules above. We support nested lists. * For a map, return a `Map` with values following the rules above. ### Hit a particular key You can enter any Selenium key or key-combination using the `Enter` task. You may also want to use the `Hit` task for more readability. The `Hit` interaction class is similar to the `Enter` class, but takes a list of one or more `Keys` values: ```java dina.attemptsTo(Enter.theValue("Sarah-Jane").into(By.id("firstName"))); dina.attemptsTo(Hit.the(Keys.TAB).into(By.id("firstName"))); ``` ### Working with checkboxes You can check or uncheck a checkbox element by clicking on it using the `Click` interaction. If you want to be sure that that the element is checked or unchecked, you can also use the `SetCheckbox` class, to either check or uncheck the field. For example, to check the Terms & Conditions checkbox on a form, you might use the following: ```java dina.attemptsTo(SetCheckbox.of("#terms-and-conditions").toTrue()); ``` And to uncheck it, you could do this: ```java dina.attemptsTo(SetCheckbox.of("#terms-and-conditions").toFalse()); ``` Since enabling and disabling checkboxes involves clicking, we can also use the `afterWaitingUntilEnabled()` and `afterWaitingUntilPresent()` methods available with the `Click` interaction class, e.g. ```java dina.attemptsTo( SetCheckbox.of("#terms-and-conditions").toFalse() .afterWaitingUntilEnabled()); ``` ### JavaScriptClick Sometimes it is useful to be able to bypass Selenium and perform a click() directly with JavaScript. We can do this with the `JavaScriptClick` class. The class has the same API as the `Click` class, e.g. ```java dina.attemptsTo(JavaScriptClick.on("#button")); ``` ### Moving the mouse We can move the mouse to an element on the page using `MoveMouse`, e.g. ```java dina.attemptsTo(MoveMouse.to("#button")); ``` If we need to perform one or more actions on the element once we have moved the cursor over it, we can do that using the `andThen()` method with a Lambda expression, which is passed the Actions object. For example, to click on the button after moving the cursor to it, we could do the following: ```java dina.attemptsTo(MoveMouse.to(BUTTON).andThen(actions -> actions.click())); ``` ### Performing actions on collections of elements We can also perform actions on a collection of elements. Suppose we have an HTML page containing a list of checkboxes like this: ```html
Salt Pepper Sauce
``` We could check each of these checkboxes in one go by using the `PerformOn.eachMatching()` method. This takes two parameters: - A locator (a Target, `By` locator, or CSS or XPath expression) - A lambda expression that accepts a `WebElementFacade` If we wanted to click on each of these checkboxes, we could do the following: ```java dina.attemptsTo( PerformOn.eachMatching(".condiment", WebElementFacade::click) ); ``` You can also use Screenplay performables, as shown in this example: ```java dina.attemptsTo( PerformOn.eachMatching(".condiment", checkbox -> dina.attemptsTo(SetCheckbox.of(checkbox).toTrue())) ); ``` ### RightClick You can right-click on an element using the `RightClick` class. This will use the Selenium `contextClick()` action method: ```java dina.attemptsTo(RightClick.on("#button")); ``` ### Scrolling elements into view Sometimes it can be useful to scroll to a specific element on the page. You can do this with the `Scroll` class, like this, which uses Javascript to scroll the element into view. ```java dina.attemptsTo(Scroll.to(By.id("#button"))); ``` This class uses the Javascript `scrollIntoView()` method, which by default will scroll the screen so that the top of the element will be aligned to the top of the visible area of the scrollable ancestor. You can also align the bottom of the element to the bottom of the visible area of the scrollable ancestor, using the `andAlignToBottom()` method, e.g. ```java dina.attemptsTo(Scroll.to(By.id("#button")).andAlignToBottom()); ``` ### Selecting from dropdowns We can select a value from a dropdown using the `SelectFromOptions` class. Suppose we have the following HTML code: ```html ``` We can select the second of these options in any of the following ways: ```java dina.attemptsTo(SelectFromOptions.byVisibleText("Green").from("#color")); dina.attemptsTo(SelectFromOptions.byValue("green").from("#color")); dina.attemptsTo(SelectFromOptions.byIndex(1).from("#color")); ``` We can retrieve the current value of a dropdown list using the `SelectedValue` Question class, e.g. ```java String selectedValue = dina.asksFor(SelectedValue.of(COLOR_"#color"))); ``` ### Switching to another window or frame We can switch to a new window or tab using the `Switch` class. For example, the following code opens a link in a new tab and switches control to this tab: ```java dina.attemptsTo( Click.on("#link-that-opens-a-new-tab"), Switch.toNewWindow() ); ``` If there are only two windows or tabs open, we can switch back to the original window by using the `Switch.toTheOtherWindow()` method: ```java dina.attemptsTo( Switch.toTheOtherWindow() ); ``` Another option is to use the name or handle of the window you want to switch to, using `Switch.toWindow()`: ```java dina.attemptsTo( Switch.toWindow(originalWindowHandle) ); ``` Alternatively, if you know the title of the window or tab, you can use the `Switch.toWindowTitled()` method: ```java dina.attemptsTo( Switch.toWindowTitled("The other window") ); ``` Although HTML frames are considered obsolete for modern applications, they are still around in some older applications. You can interact with frames using the following methods: Other Selenium switch functions are handled by the following methods: | Interaction | Purpose | Selenium equivalent | | ----------- | -------- | ----------- | | Switch.toFrame(index) | Select a frame by its (zero-based) index. Selecting a frame by index is equivalent to the JS expression window.frames[index] where "window" is the DOM window represented by the current context. Once the frame has been selected, all subsequent calls on the WebDriver interface are made to that frame. | `driver.switch().toFrame(index)` | | Switch.toFrame(nameOrId) | Select a frame by its name or ID. Frames located by matching name attributes are always given precedence over those matched by ID. | `driver.switch().toFrame(nameOrId)` | | Switch.toParentFrame() | Change focus to the parent context. If the current context is the top level browsing context, the context remains unchanged. | `driver.switchTo().parentFrame()` | | Switch.toDefaultContext() | Selects either the first frame on the page, or the main document when a page contains iframes. | `driver.switch().toDefaultContext()` | Other Selenium switch functions are handled by the following methods: | Interaction | Purpose | Selenium equivalent | | ----------- | -------- | ----------- | | Switch.toActiveElement() | Switches to the element that currently has focus within the document currently "switched to", or the body element if this cannot be detected. This matches the semantics of calling "document.activeElement" in Javascript. | `driver.switchTo().activeElement()` | ### Dealing with Alert dialogs We can work with HTML Alert dialogs using the `Switch.toAlert()` method. For example, the following code will switch the current window to the current alert dialog: ```java dina.attemptsTo( Switch.toAlert() ); ``` We can query the text of the alert using the `HtmlAlert.text()` Question class: ```java dina.attemptsTo( Switch.toAlert() ); assertThat(dina.asksFor(HtmlAlert.text())).isEqualTo("Are you sure?"); ``` We can also accept or dismiss the alert message using the `Switch.toAlert().andAccept()` and `Switch.toAlert().andDismiss()` methods respectively: ```java dina.attemptsTo( Switch.toAlert().andAccept() ); ``` ### Upload The simplest way to upload a file to an HTML upload field (one that has a type of 'file') is to use the `Upload` task. Suppose we have the following HTML form field: ```html ``` We can upload a file to this field as shown here: ```java Path fileToUpload = ... dina.attemptsTo(Upload.theFile(fileToUpload).to("#upload-file")); ``` If you are running the tests on a remote machine, you can use the Selenium Local File Detector. The Local File Detector allows the transfer of files from the client machine to the remote server. For example, if a test needs to upload a file to a web application, a remote WebDriver can automatically transfer the file from the local machine to the remote web server during runtime. This allows the file to be uploaded from the remote machine running the test. ```java Path fileToUpload = ... dina.attemptsTo(Upload.theFile(fileToUpload).to("#upload-file").usingLocalFileDetector()); ``` You can also upload a file on the classpath resources using the `Upload.theClasspathResource()` method, as shown here: ```java Path fileToUpload = ... dina.attemptsTo(Upload.theClasspathResource("some/resource/path.txt").to("#upload-file")); ``` ## Working with Chrome DevTools Many browsers provide "DevTools" – a set of tools that are integrated with the browser that developers can use to debug web apps and explore the performance of their pages. Google Chrome’s DevTools make use of a protocol called the Chrome DevTools Protocol (or “CDP” for short). As the name suggests, this is not designed for testing, nor to have a stable API, so functionality is highly dependent on the version of the browser. In Serenity Screenplay, we can access the Selenium 4 DevTools library using the `WithDevTools.perform()` method. For example: ```java final List metricList = new ArrayList<>(); dina.attemptsTo( WithDevTools.perform( devTools -> { devTools.createSession(); devTools.send(Performance.enable(Optional.empty())); metricList.addAll(devTools.send(Performance.getMetrics())); } ) ); ``` If we want to use DevTools to retrieve a specific value, we can use the `DevToolsQuery` class: ```java List metrics = dina.asksFor( DevToolsQuery.ask().about(devTools -> { devTools.createSession(); devTools.send(Performance.enable(Optional.empty())); return devTools.send(Performance.getMetrics()); }) ); ``` ## Querying the web page Serenity Screenplay also gives you a large number of options when it comes to querying a web UI. Most involve special types of Question class. In Screenplay web tests, you can simply implement a question which returns the object type you are interested, and then query the UI in a conventional Webdriver way. For example, suppose we want to read the user name on a page, which can be located with the ".user-name" CSS selector. Screenplay assertion to check the user name might look like this: ```java sam.should(seeThat(TheUserName.value(), equalTo("sam"))); ``` We could create a TheUserName question class to query this field as follows: ```java @Subject("the displayed username") public class TheUserName implements Question { @Override public String answeredBy(Actor actor) { return BrowseTheWeb.as(actor).findBy(".user-name").getText(); } public static Question value() { return new TheUserName(); } } ``` Here we use BrowseTheWeb.as(actor) to get the Serenity WebDriver API for the actor’s webdriver instance, which gives access to the full range of Serenity Page Object methods. We could also use a Target to locate the user name, which we could store in a separate Page Component class: ```java public static Target USER_NAME = Target.the("User name").locatedBy(".user-name"); ``` We can then use the resolveFor() method to find the element matching that target in the actor’s browser: ```java @Subject("the displayed username") public class TheUserName implements Question { @Override public String answeredBy(Actor actor) { return USER_NAME.resolveFor(actor).getText(); } public static Question value() { return new TheUserName(); } } ``` Alternatively, we could write this class as a factory, and use a lambda expression instead of a fully blown Question class: ```java public class TheUserName { public static Question value() { return actor -> USER_NAME.resolveFor(actor).getText(); } } ``` In this case, the `@Subject` annotation will have no effect, so we need to pass in the name of the object we are checking in the Screenplay assertion: ```java sam.should(seeThat("the displayed username", TheUserName.value(), equalTo("sam"))); ``` So far you have seen how Screenplay questions work in detail. This will help you implement your own if you need to. However Serenity also provides a number of shortcuts related to querying web pages, which you will find in the net.serenitybdd.screenplay.questions package, which allow you to write much more concise automation code. ## Bundled WebDriver Questions Serenity provides many bundled Question classes in the `net.serenitybdd.screenplay.questions` package that a short-hand way of querying a web page. For example, the `Text` class lets us retrieve the text content of an element, like this: ```java String name = sam.asksFor(Text.of("#name")); ``` If there are many matching entries, we can retrieve them all using the `ofEach()` method: ```java Collection names = sam.asksFor(Text.ofEach(".name")); ``` The full list of Web Question classes is provided here. ### Absence Determine whether an element is _not_ present or visible on the page. A field that is present in the dom but is not rendered will be considered absent. ```java boolean isNotPresent = sam.asksFor(Absence.of("#no-such-field")); ``` ### Attribute Check the value of an HTML attribute of a specified element. ```java String placeholderText = sam.asksFor(Attribute.of(".new-todo").named("placeholder")); ``` ### CheckboxValue Determine whether a checkbox has been checked or not. ```java boolean termsAndConditionsApproved = sam.asksFor(CheckboxValue.of("#tnc")); ``` ### CSSValue Retrieve the value of a specific CSS attribute of an element. ```java String font = sam.asksFor(CSSValue.of(target).named("font")); ``` ### CurrentlyEnabled Check whether an element is currently enabled, without waiting. ```java boolean isCurrentlyEnabled = sam.asksFor(CurrentlyEnabled.of("#some-button")); ``` ### CurrentVisibility Check whether an element is currently visible, without waiting. ```java boolean isCurrentlyVisible = sam.asksFor(CurrentVisibility.of("#some-button")); ``` ### Disabled Check whether an element is disabled. ```java boolean isDisabled = sam.asksFor(Disabled.of("#a-disabled-button")); ``` ### Displayed Check whether an element is displayed. If the element is not currently displayed, the test will wait for a short delay to give it time to be displayed. ```java boolean isDisplayed = sam.asksFor(Displayed.of("#some-button")); ``` ### Enabled Check whether an element is enabled. If the element is not currently enabled, the test will wait for a short delay to give it time to be enabled. ```java boolean isEnabled = sam.asksFor(Enabled.of("#some-button")); ``` ### Presence Check whether an element is present in the DOM. An invisible or hidden element can still be present in the DOM. ### SelectedStatus Alternative to `CheckboxValue` ```java boolean termsAndConditionsApproved = sam.asksFor(SelectedStatus.of("#tnc")); ``` # Text To fetch the text value of an element, we can use the `Text` class: ```java String introductionText = sam.asksFor(Text.of("#introduction")); ``` ### TextContent In some cases, we may need to read the `textContent` HTML property to get the text we need. To do this, we can use the `TextContent` class rather than `Text`. This returns the text content of the specified element, and all its descendants. ### TheCoordinates Return the coordinates of a specified element. ### TheLocation Return where on the page is the top left-hand corner of the rendered element. ### TheSize What is the width and height of the rendered element? ### Value Return the HTML `value` attribute of a specified element. ### Visibility Determine whether this web element present and visible on the screen ### Working with Dropdowns We can query the current selected value or values of an HTML `