Serenity BDD and the Screenplay Pattern

Designing SOLID Actors

Jan Molak
Jan Molak
Published in
7 min readJul 25, 2016

--

Our recent London Tester Gathering workshop on “BDD with Serenity” has met with some amazing feedback. We’re really glad that the ideas behind the Screenplay Pattern and its implementation in Serenity BDD help you keep the code structure clean, maintainable and easy to scale!

As the Screenplay Pattern grows in popularity, it’s natural that more questions and more sophisticated use cases emerge in the wild.

In this article I’d like to answer some of the most popular ones related to designing Screenplay Actors and making them SOLID.

The Role of an Actor

Here’s the first question:

I have a system where users can have different roles (e.g. author, editor, admin) and can see different sections of the webpage. Would these roles each have dedicated Actors? Or would I rather create Abilities that can reach those sections and assign them to generic Actors as needed?

In the Screenplay Pattern, an Actor represents either a person or an external system verifying and interacting with the system we’re testing.

The reason why we tend to model our Actors as separate entities and associate them with distinct personas, is also the reason why Actors have names. Let’s look into this in more detail.

Consider the following example:

Actor aurelia = Actor.named("Aurelia"); // Aurelia, the Author
Actor edward = Actor.named("Ed"); // Ed, the Editor
Actor adam = Actor.named("Adam"); // Adam, the Administrator

Here we’re giving our Actors names that sound similar to the name of the Role they perform. This makes it easier not only to distinguish one Actor from another, but more importantly — to spot logical errors in the tests: “Hey, why is Aurelia the Author touching the admin panel??”

Of course, giving an Actor a name associated with their persona serves indicative purposes only. It’s not a hard constraint that could be verified at either compile or runtime, but neither it’s intended to be.

Having said that, if we needed to be a bit more strict about what Actors can and cannot do in our acceptance tests, we could model those constraints using Abilities. This leads us onto the second question.

The Responsibilities of an Actor

How would I store username and password of an Actor? I’d like to be able to log in and check a login state later, i.e. “Logged in as: username”?

Before we answer the question of how to store additional data on an Actor, it’s worth asking: do we actually need to do this?

There are several ways we could go about solving the authentication problem mentioned in the question. Let’s talk about the most popular ones and start with discussing an inheritance-based implementation first, talk about its pros and cons and then look at a composition-based approach and see what it has to offer.

Inheritance-based approach

To solve the problem using inheritance we can extend and build on the Screenplay Actor class, adding any additional methods and data we need:

class RegisteredUser extends net.serenitybdd.screenplay.Actor {
private final String username;
private final String password;

public RegisteredUser(
String name,
String username,
String password)
{
super(name);
this.username = username;
this.password = password;
}
public String username() {
return username;
}
public String password() {
return password;
}
}

With such RegisteredUser class in place, we could instantiate Aurelia like this:

RegisteredUser aurelia = new RegisteredUser(
"Aurelia", "aurelia@example.com", "P@ssw0rd");

and ask her to LogIn into the system:

aurelia.attemptsTo(LogIn.withCredentials());

using the below LogIn Task, which retrieves the username and password stored on the Actor and passes them onto the Enter Interaction, which enters them into the LoginForm fields.

public class LogIn implements net.serenitybdd.screenplay.Task {
public static LogIn withCredentials() {
return instrumented(LogIn.class);
}

@Override
@Step("Logs in as: {0}")
public void <T extends Actor> performAs(T user) {
user.attemptsTo(
Enter.theValue(registered(user).username())
.into(LoginForm.Username),
Enter.theValue(registered(user).password())
.into(LoginForm.Password),
Click.on(LoginForm.LogInButton)
);
}

private RegisteredUser registered(Actor actor) {
return (RegisteredUser) actor;
}
}

The advantage of this approach is that it’s trivial to implement and reason about and if you only need to make the actor know about one additional piece of data, this strategy might be enough.

However, if on top of being able to authenticate using a web browser, Aurelia also needed to know how to log in to an FTP server, an email server, authenticate with a REST API and also simulate her finger touching a fingerprint sensor on her mobile phone — we would have ended up with a seriously bloated implementation of our RegisteredUser class.

We would have also violated the Interface Segregation Principle.

Could we do better? Sure we could.

Composition-based approach

So let’s talk about composition.

You might remember that every Actor in the Screenplay Pattern needs Abilities to enable them to perform their Tasks and achieve their Goals.

Actors need Abilities to enable them to perform their Tasks and achieve their Goals.

Those Abilities enable the Actor to perform Interactions and interact with the System under test:

Domain model of the Screenplay Pattern

The Screenplay Pattern is very flexible and not tied to any particular interface. This means that the Abilities could enable an Actor to “browse the Web”, “interact with a REST Api”, “send emails”, “upload files to an FTP server” and so on.

The Screenplay Pattern is very flexible and not tied to any particular interface of the system under test.

Also, the purpose of separating Abilities, such as “browsing the web”, from Tasks like “logging in”, is to introduce a translation layer and bridge the context of the business domain: “Aurelia attempts to log in” and the implementation domain: “The browser sends a POST request with Aurelia’s username and password”.

All this means that what Aurelia can do could be defined using code similar to this example:

Actor.named("Aurelia")
.whoCan(BrowseTheWeb.with(browser))
.whoCan(UploadFiles.to(ftpServerUrl));

We could take this idea further though.

An Ability defines what an Actor can do, not who they are.

We could think that being able to Authenticate is an Ability of an Actor, something they can do rather than who they are:

Actor.named("Aurelia")
.whoCan(Authenticate.with("aurelia@example.com", "P@ssw0rd"))
.whoCan(BrowseTheWeb.with(browser))
.whoCan(UploadFiles.to(ftpServerUrl));

This way we no longer need the custom Actor class. Instead, we’d store the username and password within an instance of the Ability to Authenticate:

public class Authenticate 
implements net.serenitybdd.screenplay.Ability
{
private final String username;
private final String password;

// instantiates the Ability and enables fluent DSL
public static Authenticate with(
String username,
String password)

{
return new Authenticate(username, password);
}

// retrieves the Ability from an Actor within the Interaction
public static Authenticate as(Actor actor) {

// complain if someone's asking the impossible
if (actor.abilityTo(Authenticate.class) == null) {
throw new CannotAuthenticateException(actor.getName());
}

return actor.abilityTo(Authenticate.class);
}

public String username() {
return this.username;
}

public String password() {
return this.password;
}
private Authenticate(String username, String password){
this.username = username;
this.password = password;
}
}

Same as with the inheritance-based example, we’ll still need a Task to LogIn, but this time it will look a bit different:

public class LogIn implements net.serenitybdd.screenplay.Task {
public static LogIn withCredentials() {
return instrumented(LogIn.class);
}

@Override
@Step("Logs in as: {0}")
public void <T extends Actor> performAs(T actor) {
actor.attemptsTo(
Enter.theValue(authenticated(actor).username())
.into(LoginForm.Username),
Enter.theValue(authenticated(actor).password())
.into(LoginForm.Password),
Click.on(LogInButton)
);
}

private Authenticate authenticated(Actor actor) {
return Authenticate.as(actor);
}
}

A call to the authenticated(actor) method retrieves the Actor’s ability to Authenticate, and with it, the username and password needed for Aurelia to log in.

The advantage of this approach is that it complies with the SOLID principles:

  • Single Responsibility Principle: The Actor, Authenticate and LogIn classes each have a single responsibility
  • Open/Closed Principle: If we needed to authenticate using a different mechanism, say OpenId for instance, we wouldn’t have to change the custom Actor class as in the inheritance-based example. Instead we’d create an AuthenticateWithOpenId Ability and a Task to LogInWithOpenId
  • Liskov Substitution Principle: keeping the Abilities separate from Interactions allows us to not only bridge the bound domain context in an elegant way, but also substitute one authentication strategy for another
  • Interface Segregation Principle: we didn’t have to pollute the Actor class with custom public methods, not related to their primary responsibility of executing Tasks
  • Dependency Inversion Principle: we depend on interfaces such as “Ability”, “Task”, “Interaction” rather than their concrete implementations. That’s why we can easily swap them in and out.

In closing

Any design, including software design, is an art of trade-offs.

Design, including software design, is an art of trade-offs, and most real world problems have more than one solution.

As you’ve seen, the inheritance-based approach to solving the problem of authentication and storing user’s credentials on a custom Actor class works well for simple use cases and often might be all you need.

The composition-based approach focuses on designing what Actors can do rather than what they are. This helps to keep the code clean and the classes small when you need to tackle the more sophisticated problems. Bear in mind though that for the simple ones, this design might result in a more complex object model.

If you have a chance, try to experiment with both and share your experience in the comments below :-) If you have more questions — get in touch!

Special thanks to John Ferguson Smart, author of Serenity BDD and “BDD in Action”, for reviewing the article and providing valuable suggestions.

Jan Molak speaks at conferences, runs training courses and workshops, and helps organisations deliver valuable, high-quality software frequently and reliably through implementing effective engineering practices. Get in touch to learn more!

Enjoyed the reading?

Please hit the💚 below so other people will see this article here on Medium.

You might like my other articles and tutorials too!

--

--

Consulting software engineer and trainer specialising in enhancing team collaboration and optimising software development processes for global organisations.