The Screenplay Pattern

The Screenplay Pattern is a user-centred model, which helps you shift the focus of automated acceptance tests from low-level interactions with the system to thinking about who the users of your system are, what is that they want to achieve by their interaction with your system and how exactly they're going to do it.

The Serenity/JS implementation of the Pattern focuses on making developers and testers more productive, by making acceptance test faster to write, cheaper to maintain and easier to scale to multiple projects and teams.

The Screenplay pattern is new to JavaScript but has been around for a while in various forms. It was originally proposed by Antony Marcano in 2007. You can learn more about the origin and history of this model in "Page Objects Refactored: SOLID Steps to the Screenplay Pattern" (by Antony Marcano, Andy Palmer, John Ferguson Smart and Jan Molak) and at the end of this article.

Screenplay by example

Examples presented in this section are based on the project described in Beyond Page Objects: Next Generation Test Automation with Serenity and the Screenplay Pattern.

The best way to illustrate the Screenplay pattern is through a practical example. Suppose we are writing some tests for a Todo application like the one you can find on the TodoMVC site. Consider the following scenario:

Feature: Add new items to the todo list

  As James (the just-in-time kinda guy)
  I want to capture the most important things I need to do
  So that I don’t leave so many things until the last minute

  Scenario: Adding the first todo item

    Given that James has an empty todo list
     When he adds Buy some milk to his list
     Then his todo list should contain Buy some milk

Let's think about this scenario from a couple of different angles:

  • Who is this for? What Role do they play?
  • Why are they here and what outcome do they hope for? What is their Goal?
  • What will they need to do to achieve their goal? What Tasks do they need to perform?
  • How will they complete each task? How will their specific Interactions with the system look like?

Role

We believe that the feature described in the above scenario will be useful to a "just-in-time kinda guy". "James" is a persona that we are using to understand this specific role.

Actor

Each Role we discover needs an Actor to play it. In Serenity/JS, an Actor is defined as follows:

let james = Actor.named('James');

Such Actor can perform Tasks:

james.attemptsTo(
    Start.withAnEmptyTodoList(),
    AddATodoItem.called('Buy some milk')
)

and ask Questions about the state of the application in order to verify its correctness:

expect(james.toSee(TodoListItems.Displayed)).eventually.equal([ 'Buy some milk' ]);

Goal

The Goal of an Actor is represented by the subject of the scenario:

Scenario: Adding the first todo item

Here, James should be able to add the first todo item to his list.

Task

Tasks represent high-level activities that an Actor needs to perform in order to achieve their Goal.

Tasks are named using the vocabulary of the Problem Domain, such as: "Pay with a default credit card", "Book a plane ticket" or "Add item to the basket".

A good example of a Task in our scenario could be to AddATodoItem.called('Buy some milk'), which mapped to a Cucumber.js step definition would look like this:

this.When(/^he adds (.*?) to his list$/, (name: string) => {
    return james.attemptsTo(
        AddATodoItem.called(itemName)
    );
});

Interaction

An Interaction is a low-level activity directly exercising the Actor's Ability to interact with a specific external interface of the system - such as a website, a mobile app or a web service.

Interactions are named using the vocabulary of the Solution Domain, such as: "Click a button", "Enter password into a form field" or "Submit JSON request".

Actors should not exercise Interactions directly in the Cucumber step definitions. Instead, a group of one of more Interactions should be encapsulated and represented as a Task. For example:


export class Login implements Task {

    performAs(actor: PerformsTasks) {

        // The Login Task is composed of three Interactions:

        return actor.attemptsTo(
            Enter.theValue('James').into(LoginForm.Username_Field),
            Enter.theValue('correct-horse-battery-staple').into(LoginForm.Password_Field),
            Click.on(LoginForm.Submit_Button)
        );
    }
}

Question

Similarly to an Interaction, a Question directly exercises Actor's Ability to interact with a specific external interface of the system - such as a website, a mobile app or a web service.

Asking a Question results in a promise that is eventually resolved to a specific value or a list of values.

Consider a "web question" used in the example scenario:

export class TodoListItems {
    static Displayed = Text.ofAll(TodoList.Items);
}

Calling Text.ofAll(...) returns a Question implementing the Question<string[]> interface.

When such question is applied to TodoList.Items defined as:

export class TodoList {
    static Items = Target.the('List of Items').located(by.repeater('todo in todos'));
}

it eventually resolves to a list of strings, representing the text of the web elements matched by the TodoList.Items Target.

All this means that we can have a Cucumber step defined as follows:

this.Then(/^.* todo list should contain (.*?)$/, (expectedItems: string) => {
    let answer = james.toSee(TodoListItems.Displayed);

    return expect(answer).to.eventually.equal(expectedItems);
});

or simply:

this.Then(/^.* todo list should contain (.*?)$/, (expectedItems: string) => {
    return expect(james.toSee(TodoListItems.Displayed)).eventually.equal(expectedItems);
});

Ability

In order to interact with the system, an Actor needs Abilities, which encapsulate interface-specific clients.

Those clients could be a web browser, a web service client, a mobile device driver and so on.

BrowseTheWeb is a good example of an Ability that ships with Serenity/JS. To assign the Ability to an Actor:

let james = Actor.named('James').whoCan(BrowseTheWeb.using(protractor.browser);

Once the Actor has the Ability, it can be used in an Interaction, such as Click.on(LoginForm.Submit_Button):

export class Click implements Interaction {
    public static on(target: Target): Click {
        return new Click(target);
    }

    performAs(actor: PerformsTasks & UsesAbilities): PromiseLike<void> {
        return BrowseTheWeb.as(actor).locate(this.target).click();
    }

    constructor(private target: Target) { }
}

or a Question, such as Text.of(Address.Post_Code):

export class Text implements Question<string> {

    public static of(target: Target): Text {
        return new Text(target);
    }

    answeredBy(actor: UsesAbilities): PromiseLike<string[]> {
        return BrowseTheWeb.as(actor).locate(this.target).getText();
    }

    constructor(private target: Target) {
    }
}

The Model

Serenity BDD Scenario Report with Interactions only The Screenplay Pattern domain model, from "Designing SOLID Actors" by Jan Molak

The History

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 library written in Java.

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


Your feedback matters!

Suggest features and improvements on github, get in touch on twitter, and if you found Serenity/JS useful - don't forget to give it a star! ★

Star

results matching ""

    No results matching ""