The Screenplay Test Design Pattern, also known as the Flow Pattern, has been around since 2007 and is brilliantly described by Antony Marcano, Andy Palmer, Jan Molak and John Ferguson Smart in their article Page Objects Refactored: SOLID Steps to the Screenplay/Journey Pattern. They have an excellent explanation about the concept of this pattern and what common problems it intends to address. The main issue is that the commonly used Page Object pattern can introduce a high maintenance cost if the test automation solution grows. Thus, a need for refactoring is inevitable and here is where the Screenplay Test Design Pattern comes to our rescue as it encourages engineers to write code according to the SOLID principles. More on this in the article linked above.
Most of the articles on the Internet about the Screenplay Test Design Pattern are implementations using Serenity.js. This is a design pattern like any other, so it doesn’t depend on a certain framework. The structure of the solution and the way of thinking about the users and their actions are the key concepts to reaping the benefits of this implementation model. The solution I’m presenting is written in Node.JS, with WebdriverIO and Cucumber as the test runner. I encourage you to try out the implementation on your project or use other frameworks even for the sake of learning something new. The thinking pattern is the same.
There is one more thing I need to mention before we roll up our sleeves and begin. In this demo, the application we are testing is Wordpress, so you’ll need to install it locally in order to be able to run tests in the solution. Don’t worry! It’s a quick and easy install, just follow the steps described on the Wordpress Codex page (if you’re a Windows user see this page – the installation is almost the same).
The Structure
Let’s dive into it, this is what the structure looks like:
>functionalUiTests >--actions >----interactions.js >----interrogations.js >--actors >----actor.js >----actorData.js >----admin.js >----subscriber.js >--features >----postsPage.feature >--pageobjects >----loginPage.js >----page.js >----postsPage.js >----sideNav.js >--stepDefinitions >----given.js >----then.js >----when.js
While looking through the code you’ll notice that some terms or conventions have been changed. I didn’t see the point of using human names instead of user roles. I also used interrogations
instead of questions
since that seemed too informal to me. Nonetheless the structure is the same as described by the 4 gents mentioned at the beginning of the article.
Another thing you might notice is the pageObjects
folder. Since this development model is a refined version of the Page Object model I don’t see these as excluding one another, just like you would use any other design patterns that complement each other.
The Github repo can be found here. Feel free to use it as a boilerplate project.
The Actors
Let’s look at the actors who will perform the interactions and send the interrogations:
export default [ { username: "scrplayusr", password: process.env.WP_ADMIN_PASS, role: "admin" }, { username: "scrplayusrbasic", password: process.env.WP_BASICUSER_PASS, role: "subscriber" } ];
As you can see we have the user data hard-coded in this file because this is data that will not be altered by the test so it’s safe to have it in this file. In your case you might retrieve it from the database.
The passwords are added as environment variables.
import _ from 'lodash'; import Interactions from '../actions/interactions'; import actorData from './actorData'; export default class Actor { constructor(username) { this.username = username; let actor = this.getActorData(username); this.password = actor.password; this.role = actor.role; } getActorData(username) { return _.find(actorData, {username}); } login() { Interactions.logIntoAccount(this.username, this.password); } }
This is the definition of an actor and what it can do.
import Actor from './actor'; import Interactions from '../actions/interactions'; export default class Admin extends Actor { constructor(username) { super(username); } navigateToPostsPageViaSideNav() { Interactions.navigateToPostsPageViaSideNav(); } }
The Admin
is a type of actor and it can perform interactions that other actors cannot.
The Actions
An actor can perform interactions with the application and/ or can interrogate the state of the application:
import loginPage from '../pageObjects/loginPage'; import sideNav from '../pageObjects/sideNav'; /** * Possible interactions with the applications. * The level of detail expressed here depends on the project needs. */ export default new class Interactions { /** * Logs the user into his account based on user's credentials. * @param {string} username The user's account username. * @param {string} password The user's account password. */ logIntoAccount(username, password) { // This dynamic wait is necessary when running multiple tests // one after the other. loginPage.loginForm().waitForVisible(); loginPage.usernameField().setValue(username); loginPage.passwordField().setValue(password); loginPage.loginButton().click(); } /*** Navigates to the Posts page by clicking the Posts link in the sideNav. */ navigateToPostsPageViaSideNav() { sideNav.postsLink().click(); } }
Here are interactions that actors can perform on the application. Each actor’s ability (permission) to perform an interaction is defined in the actor type class.
import postsPage from '../pageObjects/postsPage'; import sideNav from '../pageObjects/sideNav'; export default { checkExistenceOfPostsPageTitle() { return postsPage.postsPageTitle().isExisting(); }, checkExistenceOfPostsLinkInSideNav() { return sideNav.postsLink().isExisting(); }, checkExistenceOfSideNavMenu() { return sideNav.sideNavContainer().isExisting(); } }
In the Interrogations
file we define what questions
can be asked about the application’s state.
The Step Definitions
import expect from 'expect'; import { Given } from 'cucumber'; import loginPage from '../pageObjects/loginPage'; import accessPage from '../pageObjects/page'; import interrogations from '../actions/interrogations'; import Admin from '../actors/admin'; import Subscriber from '../actors/subscriber'; Given(/^(.*) user with (admin|subscriber) role authenticates into WP-Admin$/, (username, role) => { let user; if (role === 'admin') { user = new Admin(username); user.login(); } else if (role === 'subscriber') { user = new Subscriber(username); user.login(); } else { throw new Error("The user role does not exist."); } browser.params.scenarioContext.currentUser = user; // A check that the login action has actually happened is needed here. expect(interrogations.checkExistenceOfSideNavMenu()).toBeTruthy(); }); Given(/^the login page is loaded$/, () => { accessPage(loginPage.path()); });
In the step definition files we instantiate the users and put together the interactions and interrogations so that we compose the user flows.
An Overview of the Play
Now that we’ve seen each individual component let’s see the overall play: the actor performs interactions on the application in order to see an outcome and then interrogates the system about its state. The chaining of the interactions and interrogations is done in the step definition files, while the feature files constitute the whole orchestration. It’s actually beautiful how it all blends in. You can reap the benefits of this development pattern if your application supports multiple types of user roles with different functionalities available to each. This way you can reduce maintenance overhead. On the other side if your application provides the same functionality to all users or the number of different user roles is reduced then it might not make sense to put in the effort of developing tests using this pattern.
The Screenplay test design pattern can really improve your test automation solution, so I encourage you to use it, if it makes sense in your specific situation. This is not automagic (sadly), but certainly a good pattern to know and use along with others that complement it. If you’ve used this model I’d love to hear your story. Hopefully it’s a success story!
Sergiu Bacanu
Related Posts
-
Using the Page Object Design Pattern in Sencha Test
Don’t Repeat Yourself (DRY) is a software development principle that is equally important to automation…
-
Using the Page Object Design Pattern in Sencha Test
Don’t Repeat Yourself (DRY) is a software development principle that is equally important to automation…