Welcome to our latest series of How To articles on EuroSTAR Huddle. This three part series is on Understanding Screenplay and is brought to you by Matt Wynne, one of the world’s leading BDD practitioners and co-founder of Cucumber.
Part 3
In the previous post in this series, we explored the need for a new kind of pattern for organising our test automation code.
Now we’re going to work with this little codebase to refactor it towards the Screenplay pattern. By taking the existing code and shifting it, step-by-step, towards the pattern, my hope is that you’ll see how you could do this to your own code, should the fancy take you.
If you want to follow along, you can start from this commit in the example code.
Where do we start?
When refactoring to Screenplay, I’ve found it’s best to start with the smallest elements of behaviour first. The leaf-nodes, if you will. So we need to look for a helper method that just does one thing.
Let’s pick the createAccount
helper method in the DomainDriver
:
<code data-lang="javascript"> <span class="nx">createAccount</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">}</span></code>
We call this method twice from our steps. First, we use it for creating accounts:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">createAccount</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">})</span></code>
Then, later, we use it as part of signing up:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has signed up</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">createAccount</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="k">this</span><span class="p">.</span><span class="nx">activateAccount</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">})</span></code>
We’ll work with the first instance to turn this into a Screenplay interaction. We’ll come back to the second one later.
Let’s inline the first instance so that…
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">createAccount</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">})</span></code>
becomes…
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">})</span></code>
Now we have the code back up in the step definition, we can re-shape it into a Screenplay style.
Extract Interaction
We’ll put the code that does this work into an arrow function expression which will become our first Interaction. The interaction takes all the dependencies and information it will need to do its work. In this case, it needs a reference to the app
it will use to fetch the accounts, and the name
of the actor.
To complete this first refactoring, we call the interaction function, passing the name from the step definition and a reference to the app
.
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">interaction</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">app</span> <span class="p">})</span> <span class="o">=></span> <span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="nx">interaction</span><span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="na">app</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">app</span> <span class="p">})</span> <span class="p">})</span></code>
Now the code does the same as it did before. We can run npm test
at this point to check all our scenarios are still passing.
Actors perform Interactions using Abilities
Back in the first part of this series, we talked about the metaphor for Screenplay being actors on a stage.
In the metaphor, the actor is said to have abilities: the things it needs to be able to perform the actions it’s given. In practice, these are the dependencies that the interaction functions will expect to be passed, like a browser
or a database
connection. We’ll take these as we construct the Actor
.
We’ll add a method that attempts to perform an interaction, by simply passing these abilities to the interaction and calling it:
<code data-lang="javascript"><span class="kd">class</span> <span class="nx">Actor</span> <span class="p">{</span> <span class="kd">constructor</span><span class="p">(</span><span class="nx">abilities</span><span class="p">)</span> <span class="p">{</span> <span class="k">this</span><span class="p">.</span><span class="nx">abilities</span> <span class="o">=</span> <span class="nx">abilities</span> <span class="p">}</span> <span class="nx">attemptsTo</span><span class="p">(</span><span class="nx">interaction</span><span class="p">)</span> <span class="p">{</span> <span class="nx">interaction</span><span class="p">(</span><span class="k">this</span><span class="p">.</span><span class="nx">abilities</span><span class="p">)</span> <span class="p">}</span> <span class="p">}</span></code>
Perfect. Now, the actor has everything it needs to be able to attempt to perform an action.
Here’s how we use the shiny new Actor
in our step definition:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{word} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">interaction</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">app</span> <span class="p">})</span> <span class="o">=></span> <span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="kd">const</span> <span class="nx">actor</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Actor</span><span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="na">app</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">app</span> <span class="p">})</span> <span class="nx">actor</span><span class="p">.</span><span class="nx">attemptsTo</span><span class="p">(</span><span class="nx">interaction</span><span class="p">)</span> <span class="p">})</span></code>
To construct the actor, we’re creating a bare JavaScript object representing the actor’s abilities to pass to the constructor; in this case their name
and whatever attributes are on this
.
Then we call attemptsTo
on the actor, passing it our interaction.
Again, we can run npm test
at this point to check all our scenarios are still passing.
Using a custom parameter type
Since the Screenplay pattern centres around these Actors, we don’t want to have to keep constructing them in our step definitions. Fortunately, Cucumber gives us custom parameter types that allows us to transform the text Sue
, Tanya
or Bob
into an instance of Actor
that represents them.
Here’s how we do that:
<code data-lang="javascript"><span class="kd">const</span> <span class="p">{</span> <span class="nx">defineParameterType</span> <span class="p">}</span> <span class="o">=</span> <span class="nx">require</span><span class="p">(</span><span class="dl">'</span><span class="s1">cucumber</span><span class="dl">'</span><span class="p">)</span> <span class="nx">defineParameterType</span><span class="p">({</span> <span class="na">name</span><span class="p">:</span> <span class="dl">'</span><span class="s1">actor</span><span class="dl">'</span><span class="p">,</span> <span class="na">regexp</span><span class="p">:</span> <span class="sr">/</span><span class="se">(</span><span class="sr">Sue|Tanya|Bob</span><span class="se">)</span><span class="sr">/</span><span class="p">,</span> <span class="na">transformer</span><span class="p">:</span> <span class="kd">function</span><span class="p">(</span><span class="nx">name</span><span class="p">)</span> <span class="p">{</span> <span class="k">return</span> <span class="k">new</span> <span class="nx">Actor</span><span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="na">app</span><span class="p">:</span> <span class="k">this</span><span class="p">.</span><span class="nx">app</span> <span class="p">})</span> <span class="p">}</span> <span class="p">})</span></code>
Now we can ask for an instance of Actor
in our step definition, by using the new custom parameter type:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{actor} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">actor</span><span class="p">)</span> <span class="p">{</span> <span class="kd">const</span> <span class="nx">interaction</span> <span class="o">=</span> <span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">app</span> <span class="p">})</span> <span class="o">=></span> <span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="nx">actor</span><span class="p">.</span><span class="nx">attemptsTo</span><span class="p">(</span><span class="nx">interaction</span><span class="p">)</span> <span class="p">})</span></code>
Naming our Interaction
An idiom that Screenplay’s original authors used was to adopt a fluent interface for creating interactions. Let’s rename our interaction in this style, calling it CreateAccount.forThemselves
. We do this by creating a plain JavaScript object, CreateAccount
, with a property forThemselves
that returns the interaction function expression:
<code data-lang="javascript"><span class="kd">const</span> <span class="nx">CreateAccount</span> <span class="o">=</span> <span class="p">{</span> <span class="na">forThemselves</span><span class="p">:</span> <span class="p">({</span> <span class="nx">name</span><span class="p">,</span> <span class="nx">app</span> <span class="p">})</span> <span class="o">=></span> <span class="nx">app</span><span class="p">.</span><span class="nx">accounts</span><span class="p">[</span><span class="nx">name</span><span class="p">]</span> <span class="o">=</span> <span class="k">new</span> <span class="nx">Account</span><span class="p">({</span> <span class="nx">name</span> <span class="p">})</span> <span class="p">}</span></code>
Now we can use that in our step definition:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{actor} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="kd">function</span> <span class="p">(</span><span class="nx">actor</span><span class="p">)</span> <span class="p">{</span> <span class="nx">actor</span><span class="p">.</span><span class="nx">attemptsTo</span><span class="p">(</span><span class="nx">CreateAccount</span><span class="p">.</span><span class="nx">forThemselves</span><span class="p">)</span> <span class="p">})</span></code>
In fact, since we’re no longer using this
, we can now use an arrow function for our step definition:
<code data-lang="javascript"><span class="nx">Given</span><span class="p">(</span><span class="dl">'</span><span class="s1">{actor} has created an account</span><span class="dl">'</span><span class="p">,</span> <span class="nx">actor</span> <span class="o">=></span> <span class="nx">actor</span><span class="p">.</span><span class="nx">attemptsTo</span><span class="p">(</span><span class="nx">CreateAccount</span><span class="p">.</span><span class="nx">forThemselves</span><span class="p">)</span> <span class="p">)</span></code>
Wrapping up
Hopefully you can see things starting to fall into place. We’re delegating the work in our step definitions off to actors and interactions. The actors have abilities that enable them to perform the interactions, but they’re completely decoupled from those interactions themselves. Each interaction is a separate JavaScript function that takes the abilities and does something to the system.
This decoupling enables this pattern to scale really well. We can add new interactions easily, without creating extra dependencies or bloated helper classes.
All of this leaves us with step definitions that are much more readable than before, with a clear mapping from the plain English in our Gherkin steps to the code that carries out the step.
The real beauty of the actor-interactions model is that interactions are composable: we can build up more interesting actions out of fine-grained interactions, like putting lego pieces together.
In the Understanding Screenplay series:
Part 2: Help! Maybe my helpers aren’t so helpful after all?
Part 3: Refactoring to Screenplay
See other Test Automation Resources on EuroSTAR Huddle.