Screenplay Pattern approach using Selenium and Java

Learn what the Screenplay Pattern is, its elements, and how to run Screenplay tests in BrowserStack Real Device Cloud.

Get Started free
Screenplay Pattern approach using Selenium and Java
Home Guide Screenplay Pattern approach using Selenium and Java

Screenplay Pattern approach using Selenium and Java

The Screenplay Pattern is an advanced approach to writing automated tests focusing on clear, business-driven workflows. Unlike traditional testing models, it emphasizes roles, tasks, and outcomes, making tests more understandable and maintainable.

By implementing the Screenplay Pattern with Selenium and Java, you can improve the structure of your tests, making them more flexible and aligned with real-world user interactions.

This article will explore how to effectively implement the Screenplay Pattern using Selenium and integrate it with BrowserStack to run tests across real devices, ensuring comprehensive coverage and reliable results.

What is Screenplay Pattern?

The Screenplay Pattern is a user-centric approach to writing workflow-level automated acceptance tests. This helps automation testers to write test cases in terms of Business language.

Building blocks in Screenplay Pattern

To understand the building blocks better, let’s take an example

  1. Demouser login into the Application by entering their username and password and clicking on the Sign in button
  2. Demouser will be taken into the Homepage
  3. Demouser should see his/her name in the profile section

In the above example

  1. Demouser is the Actor
  2. Filling the username and password, then clicking on the button is the Task

Checking if the profile section is displaying the name of demouser is the Question

History of the Screenplay Pattern

The Screenplay Pattern, introduced to JavaScript in 2016 by Serenity/JS, has roots dating back to 2007, evolving through key contributions:

  • 2007-2008: Antony Marcano and Kevin Lawrence develop the Roles, Goals, Tasks, Actions model during the AAFTT workshop, inspiring early implementations like JNarrate (Java).
  • 2011-2013: Screenplay emerges in Ruby (Cuke Salad) and Java (Screenplay4j, ScreenplayJVM) with blog posts and talks formalizing its concepts.
  • 2015-2016: Serenity BDD integrates Screenplay for Java, popularizing it. Key publications and blog posts clarify its principles, and Serenity/JS introduces the pattern to JavaScript.
  • 2017-2021: Variants appear in Python (ScreenPy) and .NET (Boa Constrictor), and books like BDD in Action and a blog series further refine their application in testing frameworks.

What is Page Object Model?

The Page Object Model (POM) is a design pattern in test automation where a dedicated class represents each page of an application. This class acts as an interface, encapsulating the page’s elements and interactions. Test scripts then interact with the UI via the methods defined in these classes, promoting code reuse and readability.

The POM was introduced as testing tools like Selenium and WebDriver gained popularity, often among teams with varying levels of programming expertise.

Key Features:

  • Each application page has a corresponding class.
  • Classes contain methods representing actions that can be performed on-page components.
  • As the complexity of a page grows, the class methods also expand.

Challenges:

While POM effectively models page components, it does not inherently align with user roles, actions, and outcomes, essential for capturing business logic and user journeys. Tools like JUnit and TestNG may struggle to express these workflows in terms of business representation clearly.

Elements of the Screenplay Pattern

The Screenplay Pattern models test scenarios using a metaphor inspired by stage performances, framing tests as scripts that describe how actors interact with the system under test to achieve their objectives.

This approach helps define:

  • Who is interacting with the system?
  • Why is the interaction necessary?
  • What actions are required to achieve their goals?
  • How are those actions executed?

Core Components of the Screenplay Pattern:

  1. Actors: Represent users or external systems interacting with the application under test.
  2. Abilities: Serve as lightweight wrappers around integration libraries that interact with the system.
  3. Interactions: Represent individual, low-level actions that actors can perform via an interface.
  4. Tasks: Combine sequences of interactions into meaningful business workflow steps.
  5. Questions: Allow retrieval of information from the application and testing environment.

Screenplay Pattern Implementation in Selenium

To start implementing Screenplay Pattern, you can use Serenity BDD framework which has inbuilt integration to write our tests in the Screenplay pattern.

Note: Get all the required Maven dependencies as a pre-requisite.

To write Tests, Tasks, and Questions considering Browserstack’s demo application. The structure of the below classes can be seen below.

Structure of Classes in Screenplay Pattern Example

Here are the steps to implement the Screenplay Pattern approach in Selenium Java:

Step 1: Create PageObject in a refactored and effective manner

Login Page:

import net.serenitybdd.screenplay.targets.Target;
import net.thucydides.core.pages.PageObject;

public class BStackLoginPage extends PageObject {

public static final Target USERNAME = Target.the("Username")
.locatedBy("#username input");

public static final Target PASSWORD = Target.the("Password")
.locatedBy("#password input");

public static final Target LOGIN_BTN = Target.the("Login Button")
.locatedBy("#login-btn");
}

Dashboard Page:

import net.serenitybdd.screenplay.targets.Target;
import net.thucydides.core.pages.PageObject;

public class BstackDashboardPage extends PageObject {

public static final Target SIGNOUT = Target.the("sign out")
.locatedBy(".username");
}

Step 2: Create Tasks for the above created PageObjects

Task to access the webpage

import com.ui.screenplay.pageobject.BStackLoginPage;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.actions.Open;
import net.thucydides.core.annotations.Step;

import static net.serenitybdd.screenplay.Tasks.instrumented;

public class AccessWebPage implements Task {
public static AccessWebPage loginPage() {
return instrumented(AccessWebPage.class);
}

BStackLoginPage loginPage;

@Step("{0} access Login page")
public <T extends Actor> void performAs(T t) {
t.attemptsTo(Open.browserOn().the(loginPage));
}
}

Task to Login to the application

import com.ui.screenplay.pageobject.BStackLoginPage;
import net.serenitybdd.core.steps.Instrumented;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Task;
import net.serenitybdd.screenplay.actions.Click;
import net.serenitybdd.screenplay.actions.Enter;
import net.thucydides.core.annotations.Step;
import org.openqa.selenium.Keys;

public class LoginToBstack implements Task {

@Step("{0} enter username and password '#username' '#password")
public <T extends Actor> void performAs(T actor)
{
actor.attemptsTo(Enter.theValue(username).into(BStackLoginPage.USERNAME).thenHit(Keys.TAB));
actor.attemptsTo(Enter.theValue(password).into(BStackLoginPage.PASSWORD).thenHit(Keys.TAB));
actor.attemptsTo(Click.on(BStackLoginPage.LOGIN_BTN));
}

private String username;
private String password;

public LoginToBstack(String username, String password) {
this.username = username;
this.password = password;
}

public static Task withCredentials(String username, String password) {
return Instrumented
.instanceOf(LoginToBstack.class)
.withProperties(username, password);
}
}

Step 3: Create a Question to fetch user information from the Profile section of Homepage

import com.ui.screenplay.pageobject.BstackDashboardPage;
import net.serenitybdd.screenplay.Actor;
import net.serenitybdd.screenplay.Question;
import net.serenitybdd.screenplay.questions.Text;

public class Dashboard implements Question<String> {

public static Question<String> displayed() {
return new Dashboard();
}

public String answeredBy(Actor actor) {
return Text.of(BstackDashboardPage.SIGNOUT).answeredBy(actor);
}
}

Step 4: Create a test

import com.ui.screenplay.questions.Dashboard;
import com.ui.screenplay.tasks.LoginToBstack;
import net.serenitybdd.junit.runners.SerenityRunner;
import net.serenitybdd.screenplay.Actor;
import static net.serenitybdd.screenplay.GivenWhenThen.*;
import net.serenitybdd.screenplay.abilities.BrowseTheWeb;
import net.serenitybdd.screenplay.actions.Open;
import net.thucydides.core.annotations.Managed;
import org.hamcrest.CoreMatchers;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.openqa.selenium.WebDriver;

@RunWith(SerenityRunner.class)
public class ScreenPlayTest {

private Actor demoUser = Actor.named("Demo User");

@Managed
private WebDriver hisBrowser;

@Before
public void demoUserCanBrowseTheWeb(){
demoUser.can(BrowseTheWeb.with(hisBrowser));
}

@Test
public void browseTheWebAsDemoUser(){
demoUser.attemptsTo(Open.url("https://bstackdemo.com/signin"));
givenThat(demoUser).attemptsTo(LoginToBstack.withCredentials("demouser", "testingisfun99"));
then(demoUser).should(seeThat(Dashboard.displayed(), CoreMatchers.equalTo("demouser1")));
}

}

Step 5: Once you run the above test, the report will be generated in the path target > site > serenity > index.html

Serenity HTML Report

Screenplay with Serenity BDD not just provides better maintainability and readability of code but also produces an extensive HTML Report as seen above.

Multi-Level Task Execution in the Screenplay Pattern

The Screenplay Pattern supports task composition, allowing you to structure test actions across multiple levels of abstraction. Instead of writing long, procedural test steps, you can break down user interactions into smaller, reusable building blocks composed into larger, more expressive tasks.

This approach helps model complex user behaviors in a clean, scalable, and maintainable way.

Why Multi-Level Execution Matters?

Many user workflows involve multiple steps. For instance, placing an order typically includes logging in, adding items to the cart, applying a discount code, and completing the checkout. In the Screenplay Pattern, each of these steps can be defined as a task or interaction and then composed into a single, higher-level task.

This enables you to:

  • Write test scenarios that are easier to read and understand.
  • Keep UI-specific logic separate from business logic.
  • Reuse smaller task components across different test flows.
  • Update only affected components when the UI changes.

Example: Consider the test scenario: “A registered user places an order successfully.”

In Screenplay, this might look like:

actor.attemptsTo(

    PlaceAnOrder.withDetails(orderDetails)

);

The PlaceAnOrder task might internally perform:

public class PlaceAnOrder implements Task {

    private OrderDetails details;



    public PlaceAnOrder(OrderDetails details) {

        this.details = details;

    }



    @Override

    public <T extends Actor> void performAs(T actor) {

        actor.attemptsTo(

            Login.withCredentials(details.getUser()),

            AddItems.toCart(details.getItems()),

            ApplyDiscountCode.withCode(details.getDiscountCode()),

            ProceedToCheckout.andConfirm()

        );

    }



    public static PlaceAnOrder withDetails(OrderDetails details) {

        return Tasks.instrumented(PlaceAnOrder.class, details);

    }

}

Each of the above steps, Login, AddItems, ApplyDiscountCode, and ProceedToCheckout, can be standalone, testable tasks reused across different flows.

Talk to an Expert

Composability in Practice

This multi-level composition approach ensures your test suite remains scalable, modular, and easy to maintain even as your application grows in complexity. It allows you to:

  • Isolate low-level interactions such as Click.on(BUTTON) or Enter.theValue(“xyz”).into(FIELD) into atomic tasks that can be tested independently and reused when needed.
  • Leverage mid-level tasks like Login.withCredentials() or AddItems.toCart() across multiple scenarios without duplicating logic.
  • Combine tasks into high-level workflows such as PlaceAnOrder.withDetails() that represent complete user journeys in a readable, business-focused format.

By composing tasks this way, you achieve:

  • Better readability: Test scenarios describe what the user is doing, not how.
  • Greater reusability: Common tasks are shared across different tests, minimizing duplication.
  • Easier maintenance: When the UI changes, only the relevant task needs to be updated, not every test case.
  • Improved collaboration: Non-technical stakeholders can follow test flows without digging into implementation details.

Running Screenplay tests in BrowserStack Real Device Cloud

To get the most value out of your test automation suite, it’s essential to optimize for three key factors:

  • Stability: Tests should run reliably across different environments
  • Execution Speed: Faster feedback loops help accelerate development
  • Coverage: Tests should run across a wide range of devices and browsers

However, achieving broad test coverage is often a challenge. Performing the test through an in-house device lab comes at a high cost. Setting up and maintaining the necessary infrastructure demands significant time, effort, and ongoing investment.

BrowserStack eliminates this overhead by offering instant access to a real device cloud. You can run tests across over 3,500 real devices and browsers with minimal changes to your existing Screenplay framework. Just configure the integration and start testing under real user conditions without managing infrastructure.

Test on Real Devices & Browsers

In order to run the above Screenplay test against the BrowserStack Cloud environment follow the steps below:

Step 1: Create a New Custom driver provider

import java.net.URL;
import java.util.Iterator;

import org.openqa.selenium.WebDriver;
import org.openqa.selenium.remote.DesiredCapabilities;
import org.openqa.selenium.remote.RemoteWebDriver;

import net.thucydides.core.util.EnvironmentVariables;
import net.thucydides.core.util.SystemEnvironmentVariables;
import net.thucydides.core.webdriver.DriverSource;

public class BrowserStackSerenityDriver implements DriverSource {

public WebDriver newDriver() {
EnvironmentVariables environmentVariables = SystemEnvironmentVariables.createEnvironmentVariables();

String username = System.getenv("BROWSERSTACK_USERNAME");
if (username == null) {
username = (String) environmentVariables.getProperty("browserstack.user");
}

String accessKey = System.getenv("BROWSERSTACK_ACCESS_KEY");
if (accessKey == null) {
accessKey = (String) environmentVariables.getProperty("browserstack.key");
}

String environment = System.getProperty("environment");
DesiredCapabilities capabilities = new DesiredCapabilities();

Iterator it = environmentVariables.getKeys().iterator();
while (it.hasNext()) {
String key = (String) it.next();

if (key.equals("browserstack.user") || key.equals("browserstack.key")
|| key.equals("browserstack.server")) {
continue;
} else if (key.startsWith("bstack_")) {
capabilities.setCapability(key.replace("bstack_", ""), environmentVariables.getProperty(key));
if (key.equals("bstack_browserstack.local")
&& environmentVariables.getProperty(key).equalsIgnoreCase("true")) {
System.setProperty("browserstack.local", "true");
}
} else if (environment != null && key.startsWith("environment." + environment)) {
capabilities.setCapability(key.replace("environment." + environment + ".", ""),
environmentVariables.getProperty(key));
if (key.equals("environment." + environment + ".browserstack.local")
&& environmentVariables.getProperty(key).equalsIgnoreCase("true")) {
System.setProperty("browserstack.local", "true");
}
}
}

try {
return new RemoteWebDriver(new URL("https://" + username + ":" + accessKey + "@"
+ environmentVariables.getProperty("browserstack.server") + "/wd/hub"), capabilities);
} catch (Exception e) {
System.out.println(e);
return null;
}
}

public boolean takesScreenshots() {
return true;
}
}

Step 2: Add BrowserStack Environment setup code in the Before hook

public class BrowserStackSerenityTest {
static Local bsLocal;

@BeforeClass
public static void setUp() throws Exception {
EnvironmentVariables environmentVariables = SystemEnvironmentVariables.createEnvironmentVariables();

String accessKey = System.getenv("BROWSERSTACK_ACCESS_KEY");
if (accessKey == null) {
accessKey = (String) environmentVariables.getProperty("browserstack.key");
}

String environment = System.getProperty("environment");
String key = "bstack_browserstack.local";
boolean is_local = environmentVariables.getProperty(key) != null
&& environmentVariables.getProperty(key).equals("true");

if (environment != null && !is_local) {
key = "environment." + environment + ".browserstack.local";
is_local = environmentVariables.getProperty(key) != null
&& environmentVariables.getProperty(key).equals("true");
}

if (is_local) {
bsLocal = new Local();
Map<String, String> bsLocalArgs = new HashMap<String, String>();
bsLocalArgs.put("key", accessKey);
bsLocal.start(bsLocalArgs);
}
}

@AfterClass
public static void tearDown() throws Exception {
if (bsLocal != null) {
bsLocal.stop();
}
}
}

Step 3: Add Browserstack related configuration keys to Serenity.properties file

webdriver.driver = provided
webdriver.provided.type = mydriver
webdriver.provided.mydriver = com.ui.screenplay.BrowserStackSerenityDriver
serenity.driver.capabilities = mydriver

webdriver.timeouts.implicitlywait = 5000
serenity.use.unique.browser = false
serenity.dry.run=false
serenity.take.screenshots=AFTER_EACH_STEP

browserstack.user=gurudattananthap_jG62JF
browserstack.key=9yV8DdY2CwdWNizFWxqC
browserstack.server=hub.browserstack.com

bstack_build=browserstack-screenplay-build-1
bstack_debug=true
bstack_browserstack.console=verbose

environment.single.name=serenity_single_test
environment.single.browser=chrome

Once you run the test, the execution happens in BrowserStack Cloud. The Execution results, text, video, and console logs can be found in the BrowserStack Automate Dashboard

BrowserStack Execution Result

These tests can be easily shared with the team using Slack, JIRA, GitHub, or Trello integrations on BrowserStack for effective bug reporting.

BrowserStack Percy Banner

Conclusion

Screenplay pattern provides an effective way to organize, maintain, and refactor the PageObject classes. As the Screenplay pattern is Integrated with BDD, you don’t need to Maintain driver objects and can leverage inbuilt methods easily and effectively.

Screenplay Serenity BDD produces detailed HTML reports that can also be quickly run in the BrowserStack cloud to test under real user conditions.

Tags
Automated UI Testing Automation Testing Selenium Visual Testing

Get answers on our Discord Community

Join our Discord community to connect with others! Get your questions answered and stay informed.

Join Discord Community
Discord