The Screenplay Pattern

The Screenplay Pattern is a user-centred approach to writing high-quality automated acceptance tests. It steers you towards an effective use of layers of abstraction, helps your tests capture the business vernacular, and encourages good testing and software engineering habits.

Instead of focusing on low-level, interface-centric interactions, you describe your test scenarios in a similar way you'd describe them to a human being - an actor in Screenplay-speak. You write simple, readable and highly-reusable code that instructs the actors what activities to perform and what things to check. The domain-specific test language you create is used to express screenplays - the activities for the actors to perform in a given test scenario.

The Screenplay Pattern is one of the core design patterns behind Serenity/JS and in this chapter we'll look into how to apply it in practice.

Did you know? The Screenplay Pattern uses the system metaphor of a stage performance where each test scenario is like a little screenplay describing how the actors should go about performing their activities while interacting with the system under test.

The "screen" in "screenplay" has nothing to do with the computer screen. On the contrary, the Screenplay Pattern is a general method of modelling acceptance tests interacting with any external interface of your system. In fact, the Serenity/JS implementation of the Screenplay Pattern can help you break free from UI-only-based testing!

The Five Elements

The Screenplay Pattern is beautiful in its simplicity. It's made up of five elements, five types of building blocks that Serenity/JS gives you to design any functional acceptance test you need, no matter how sophisticated or how simple.

The key elements of the pattern are: actors, abilities, interactions, questions, and tasks.

The Screenplay Pattern
The Screenplay Pattern

Screenplay by example

The best way to illustrate the Screenplay Pattern is through a practical example, so let's continue experimenting with the TodoMVC application we've introduced in the previous chapter. And while Serenity/JS supports several test runners (including Cucumber.js, which we'll look into later on in the book), we'll use Jasmine for now to keep things simple.

If you'd like to follow along with the coding, please use the Serenity/JS-Jasmine-Protractor template.

Actors

The Screenplay Pattern is a user-centric model with users and external systems interacting with the system under test represented as actors. Actors are a key element of the pattern as they are the ones performing the test scenarios.

In Serenity/JS, an Actor is defined using an actorCalled convenience method:

// spec/todo.spec.ts

import { Actor, actorCalled } from '@serenity-js/core';

const James: Actor = actorCalled('James');

You'll notice that in the code sample above I called the actor James. While you can give your actors whatever names you like, a good pattern to follow is to give them names indicating the personae they represent or the role they play in the system. This way you can help people working on the tests quickly recall the context, constraints and affordances of a given actor by associating the name of the actor with a persona they're already familiar with.

In this case, let's say that our actor represents the persona of "James", a "just-in-time kinda guy", who's trying to get himself organised with our to-do list app.

PRO TIP: Just like the five core elements of the Screenplay Pattern, the term "role" comes from the system metaphor of a stage performance. It should be interpreted as the role a given actor plays in the performance, the role they play in the system - "a doctor", "a trader", "a writer" are all good examples of actor roles.

While a "role" often implies the permissions a given actor has in the system they interact with (i.e. a "writer" writes articles, a "publisher" publishes articles), this is not a mechanism to prevent the actor from performing activities inconsistent with their role. In particular, Serenity/JS will not prevent you from writing scenarios where a "writer" tries to impersonate a "publisher" and publish an article. If it did, you would not be able to test if your system correctly implemented its access control mechanisms!

Check out the Design Guide to learn more about actors.

Interactions

The job of an actor is to attempt to perform a series of interactions with the system under test, such as navigating to websites, clicking on buttons, entering values into form fields, or sending HTTP requests to a REST API.

This series of interactions, provided as arguments to the actor.attemptsTo(..) method, constitutes a test scenario (also referred to as an actor flow):

// spec/todo.spec.ts

import 'jasmine';

import { actorCalled } from '@serenity-js/core';
import { Navigate } from '@serenity-js/protractor';

describe('Todo List App', () => {

    it('helps engineers learn Serenity/JS', () =>
        actorCalled('James').attemptsTo(
            Navigate.to('http://todomvc.com/examples/angularjs/')
        ));
});

In the example above, you see an actor performing an Interaction to Navigate.to the TodoMVC app.

If you're following along with the coding, you can implement the above scenario under spec/todo.spec.ts.

There are two interesting points about the scenario above that I'd like to draw your attention to:

  1. The actor.attemptsTo(..) method returns a standard JavaScript Promise, which allows Jasmine to synchronise the chain of actor's interactions with the test runner. Serenity/JS does not try to pretend that the world of JavaScript is not asynchronous. Instead, it gives you patterns and tools to deal with this in an elegant way.
  2. The interaction to Navigate, just like many other common interactions you'll need to write your Web tests, ships as part of the @serenity-js/protractor module. Serenity/JS modules provide dozens of interactions to cover most of your test automation needs, and if there are any missing you can easily create them yourself and contribute back to the community 😊

Check out the Design Guide to learn more about interactions.

Abilities

If Serenity/JS is a full-stack acceptance testing framework that allows you to interact with any interface of the system, how does it know what interfaces you actually need to exercise in your test? How does it know how to connect your actors to those interfaces? Well, it doesn't unless you tell it, and that's where abilities come into play.

You can think of an Ability as a thin wrapper around a client of a specific interface you'd like the actor to use. Those abilities is what the interactions use under the hood and what enables the actor to perform interactions with the system under test.

For example, here's an ability that enables the actor to browse the Web using protractor.browser and enables Web-specific interactions such as Navigate:

import { Ability } from '@serenity-js/core';
import { BrowseTheWeb } from '@serenity-js/protractor';
import { protractor } from 'protractor';

const browseTheWeb: Ability = BrowseTheWeb.using(protractor.browser);

Now, you could give an actor this ability directly, using the actor.whoCan(..) API:

// spec/todo.spec.ts

import 'jasmine';

import { actorCalled } from '@serenity-js/core';
import { BrowseTheWeb, Navigate } from '@serenity-js/protractor';
import { protractor } from 'protractor';

describe('Todo List App', () => {

    it('helps engineers learn Serenity/JS', () =>
        actorCalled('James')
            .whoCan(BrowseTheWeb.using(protractor.browser))
            .attemptsTo(
                Navigate.to('http://todomvc.com/examples/angularjs/')
            ));
});

However, there's a more elegant way to do it that doesn't require you to repeat yourself in every test you write.

The alternative is to engage(..) a Cast of actors, who gain their abilities when they prepare for the performance before the scenario starts:

// spec/todo.spec.ts

import { Actor, actorCalled, Cast, engage } from '@serenity-js/core';
import { BrowseTheWeb, Navigate } from '@serenity-js/protractor';
import { protractor } from 'protractor';

class Actors implements Cast {
    prepare(actor: Actor): Actor {
        return actor.whoCan(
            BrowseTheWeb.using(protractor.browser),
        );
    }
}

describe('Todo List App', () => {

    beforeEach(() => engage(new Actors()));

    it('helps engineers learn Serenity/JS', () =>
        actorCalled('James').attemptsTo(
            Navigate.to('http://todomvc.com/examples/angularjs/')
        ));
});

If you're following along with the coding, you can execute the test you've just implemented by running npm test in your terminal.

Check out the Design Guide to learn more about abilities.

Questions

The fourth building block of the Screenplay Pattern is questions, and just like with interactions, questions are interface-specific and enabled by abilities. Answering a Question provides actor with information about the state of the system under test.

For example, you have a question that retrieves the title of the website:

import { Question } from '@serenity-js/core';
import { Website } from '@serenity-js/protractor';

const title: Question<Promise<string>> = Website.title()

Or the one that retrieves a web element for the actor's interactions to target:

import { Question } from '@serenity-js/core';
import { Target } from '@serenity-js/protractor';
import { by, ElementFinder } from 'protractor';

const header: Question<ElementFinder> = Target.the('header').located(by.css('h1'));

You also have higher-order questions that can be passed other questions as arguments.

For example, this is how you retrieve the text of the header web element from the sample above:

import { Question } from '@serenity-js/core';
import { Text } from '@serenity-js/protractor';

const headerText: Question<Promise<string>> = Text.of(header);

So what can you do with those questions? A great many things!

You can use them with most Serenity/JS interactions, such as the one that logs the answer to the question so that it can be printed to the terminal (useful when debugging your test scenarios):

import { Log } from '@serenity-js/core';

Log.the(headerText)

More importantly, you can also use them with assertions:

import { Ensure, equals } from '@serenity-js/assertions';

Ensure.that(headerText, equals('todos'))

As well as for synchronising your tests with the UI:

import { Wait, isPresent } from '@serenity-js/protractor';
import { equals } from '@serenity-js/assertions';

Wait.until(header, isPresent())
Wait.until(headerText, equals('todos'))

If you wanted to experiment with some of the above questions, you could expand the original test scenario as follows:

// spec/todo.spec.ts
import 'jasmine';

import { Ensure, equals } from '@serenity-js/assertions';
import { Actor, actorCalled, Cast, engage } from '@serenity-js/core';
import { BrowseTheWeb, isVisible, Navigate, Target, Text, Wait } from '@serenity-js/protractor';
import { by, protractor } from 'protractor';

class Actors implements Cast {
    prepare(actor: Actor): Actor {
        return actor.whoCan(
            BrowseTheWeb.using(protractor.browser),
        );
    }
}

// this is what I call a Lean Page Object, more on those later on in the book
class TodoListApp {
    static header = Target.the('header').located(by.css('h1'));
}

describe('Todo List App', () => {

    beforeEach(() => engage(new Actors()));

    it('helps engineers learn Serenity/JS', () =>
        actorCalled('James').attemptsTo(
            Navigate.to('http://todomvc.com/examples/angularjs/'),
            Wait.until(TodoListApp.header, isVisible()),
            Ensure.that(Text.of(TodoListApp.header), equals('todos')),
        ));
});

If you'd like to see the above scenario in action, paste the code above in spec/todo.spec.ts and run npm test in your terminal.

Check out the Design Guide to learn more about questions.

Tasks

The final piece of the puzzle is tasks. Tasks are the higher-level abstractions that model the activities an actor needs to perform in business domain terms.

For example, the three interactions we've just discussed could be modelled as a task to "launch the app":

// spec/todo.spec.ts
import 'jasmine';


import { Ensure, equals } from '@serenity-js/assertions';
import { Task } from '@serenity-js/core';
import { isVisible, Navigate, Text, Wait } from '@serenity-js/protractor';

const LaunchTheApp = () =>
    Task.where(`#actor launches the app`, 
        Navigate.to('http://todomvc.com/examples/angularjs/'),
        Wait.until(TodoListApp.header, isVisible()),
        Ensure.that(Text.of(TodoListApp.header), equals('todos')),
    )

To make an actor perform a task, you pass it to the attemptsTo(..) method, just like we did with the interactions.

// spec/todo.spec.ts
import 'jasmine';

import { Ensure, equals } from '@serenity-js/assertions';
import { Actor, actorCalled, Cast, engage, Task } from '@serenity-js/core';
import { BrowseTheWeb, isVisible, Navigate, Target, Text, Wait } from '@serenity-js/protractor';
import { by, protractor } from 'protractor';

class Actors implements Cast {
    prepare(actor: Actor): Actor {
        return actor.whoCan(
            BrowseTheWeb.using(protractor.browser),
        );
    }
}

// this is what I call a Lean Page Object, more on those later on in the book
class TodoListApp {
    static header = Target.the('header').located(by.css('h1'));
}

const LaunchTheApp = () =>
    Task.where(`#actor launches the app`,
        Navigate.to('http://todomvc.com/examples/angularjs/'),
        Wait.until(TodoListApp.header, isVisible()),
        Ensure.that(Text.of(TodoListApp.header), equals('todos')),
    )

fdescribe('Todo List App', () => {

    beforeEach(() => engage(new Actors()));

    it('helps engineers learn Serenity/JS', () =>
        actorCalled('James').attemptsTo(
            LaunchTheApp(),
            // perform other tasks, like adding items to the list
        ));
});

The great thing about Serenity/JS tasks is that you can use them to compose not just the interactions, but other tasks too! I'll show you how to do it later on in the book.

For now, you can paste the code above in spec/todo.spec.ts and run npm test in your terminal to see output similar to the below:

/Users/jan/serenity-js-experiments/spec/todo.spec.ts:32

Todo List App: helps engineers learn Serenity/JS

  James launches the app
    ✓ James navigates to 'http://todomvc.com/examples/angularjs/' (840ms)
    ✓ James waits up to 5s until the header does become visible (155ms)
    ✓ James ensures that the text of the header does equal 'todos' (18ms)

✓ Execution successful (1s 39ms)

Check out the Design Guide to learn more about tasks.


Now that you understand one of the core patterns behind Serenity/JS it's time to move on to the next one, the Lean Page Objects.

In this next article) we'll improve the test we've just written and look at ways to structure our code better.

The origins of the Screenplay Pattern

Serenity/JS introduced the Screenplay Pattern to the JavaScript world back in 2016, but the ideas behind it have been around for a while in various forms.

It all started at the Agile Alliance Functional Testing Tools workshop (AAFTT) back in 2007.

"In praise of abstraction", a talk given by Kevin Lawrence, inspired Antony Marcano to implement a fluent DSL based on Kevin's idea to use the language of Interaction Designers to model the layers of abstraction in an acceptance test. With the help of Andy Palmer, this fluent DSL is what became JNarrate a year later (2008).

In the late 2012, Antony and Andy joined forces with Jan Molak. Their experiments with Kevin's model, combined with a desire to address problems with shortcomings of the PageObject Pattern and apply SOLID design principles to acceptance testing is what became known in 2013 as screenplay-jvm.

In 2015, when Antony, Andy and Jan started working with John Ferguson Smart, what became known as the Screenplay Pattern found its way into Serenity BDD, a popular acceptance testing and reporting library written in Java.

In 2016, you can use both the Screenplay Pattern and the powerful Serenity BDD reports on JavaScript projects thanks to Serenity/JS!

If you'd like to find out more about the thought process behind the Screenplay Pattern, check out: