Screenplay Pattern approach using Selenium and Java
By Gurudatt S A, Community Contributor - February 8, 2023
What is Page Object Model?
A page object is an object-oriented class that serves as an interface to a page of your Application Under Test (AUT). The tests then use the methods of this page object class whenever they need to interact with the UI of that page.
Page Object Model was recommended at a time when Selenium and WebDriver were being used increasingly by testing teams, and not necessarily by those with extensive programming skills.
Let’s consider an application with the below use cases
- Login page contains the username, password, and login button
- After successful login, the application displays the homepage
With the Page Object Model, you need to create separate classes for each page, and each page will contain methods for each component. As the components within the page grow, the methods will also grow.
Also given any application, it will have certain roles, and those roles will perform certain actions which result in an outcome. These tests should also be designed in a similar way where you can clearly see the user journey, actions, the roles performed, and validate the outcomes.
Using the PageObject model and Test Runner like JUnit and TestNG, such tests cannot be designed with a clear definition in terms of Business representation.
The Screenplay pattern comes to the rescue when designing the tests in the Role, Tasks, and Outcome driven approach.
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
- Demouser login into the Application by entering their username and password and clicking on the Sign in button
- Demouser will be taken into the Homepage
- Demouser should see his/her name in the profile section
In the above example
- Demouser is the Actor
- 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
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.
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
Screenplay with Serenity BDD not just provides better maintainability and readability of code but also produces an extensive HTML Report as seen above.
Note: One can read the official documentation of BrowserStack to set up Serenity with Selenium.
Running Screenplay tests in BrowserStack Real Device Cloud
To get the optimum benefits of the test automation suite, the key metrics are:
- Stable Tests
- Test Execution Time
- Test Coverage: Running the tests across multiple browsers and devices
It’s not realistic for any organization to run the tests across all the browsers and devices to ensure maximum test coverage. Building an in-house infrastructure means more cost to set up and maintain the infrastructure. With BrowserStack’s real device cloud, it’s now very easy to set up your tests with minimum changes to existing code, and just by making a few configurations, you can run your tests against 3000+ device and browser combinations under real user conditions.
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
These tests can be easily shared with the team using Slack, JIRA, GitHub, or Trello integrations on BrowserStack for effective bug reporting.
Percy Integration with Screenplay for Visual Testing
Integrate Percy to implement Selenium Screenplay Pattern Approach in Visual Testing to make it more effective by following the below steps:
Step 1: Add Percy Java Selenium dependency to the project POM.xml
<dependency> <groupId>io.percy</groupId> <artifactId>percy-java-selenium</artifactId> <version>1.0.0</version> </dependency>
Step 2: Add Package.json file to the root of the Project and run the below command
npm install --save-dev @percy/cli
Step 3: Add Percy Snapshot method to your test like below
import com.ui.screenplay.hooks.BrowserStackSerenityTest; 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; import io.percy.selenium.Percy; @RunWith(SerenityRunner.class) public class ScreenPlayTest extends BrowserStackSerenityTest { private Actor demoUser = Actor.named("Demo User"); private static Percy percy; @Managed WebDriver hisBrowser; @Before public void demoUserCanBrowseTheWeb(){ demoUser.can(BrowseTheWeb.with(hisBrowser)); percy = new Percy(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("demouser"))); percy.snapshot("Bstack Homepage"); } }
Step 4: Export the PERCY_TOKEN = <Your token>. Refer to the document here to get your Percy token.
Step 5: Execute your maven/java command using Percy CLI
./node_modules/.bin/percy exec -- mvn verify
Now the test will run in BrowserStack Cloud along with Percy integration to Capture snapshots and compare the Visual Changes. A new build will be created in Percy under your project where every time you can run a Visual Regression Test.
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.