Better Automated Acceptance Tests With Serenity Screenplay
A tutorial on how to use Serenity, a library for reporting automated acceptance tests in a BDD environment.
Join the DZone community and get the full member experience.
Join For Free1. introduction
the screenplay pattern is a powerful and elegant approach to designing and implementing automated tests, providing a number of improvements over more traditional approaches such as the page objects model. the screenplay pattern uses good software engineering principles such as the single responsibility principle, the open-closed principle, favours composition over inheritance, employs thinking from domain driven design to reflect the domain of performing acceptance tests and steers you towards effective use of layers of abstraction. it encourages good testing habits and well-designed test suites that are easy to read, easy to maintain and easy to extend, enabling teams to write more robust and more reliable automated tests more effectively.
you can find some details about the origins of the screenplay pattern in this article .
in this tutorial, you will discover just how easy it is to get productive quickly with serenity and the screenplay pattern. we will introduce the core concepts behind the screenplay pattern and how it is implemented in serenity along the way.
this tutorial assumes some familiarity with java and java ides, and a passing familiarity with maven or gradle. the source code for this tutorial can be found on github .
2. getting started
the easiest way to create a project skeleton for a serenity screenplay project is to use the maven archetype plugin. to do this, run the
mvn archetype:generate
command (with a filter to reduce the number of artifacts maven proposes) as shown here:
$ mvn archetype:generate -dfilter=screenplay
...
[info] no archetype defined. using maven-archetype-quickstart (org.apache.maven.archetypes:maven-archetype-quickstart:1.0)
choose archetype:
1: remote -> net.serenity-bdd:serenity-junit-screenplay-archetype (serenity automated acceptance testing project using screenplay, selenium 2 and junit)
choose a number or apply filter (format: [groupid:]artifactid, case sensitive contains): :
this will (after downloading list all of the available serenity screenplay archetypes. for this tutorial, we will be working with junit, so enter the number corresponding to the
net.serenity-bdd:serenity-junit-screenplay-archetype
entry ("1" in the example shown here).
you will then be prompted to enter a groupid, artifactid, and version for your project, and a root package for your classes.
choose a number or apply filter (format: [groupid:]artifactid, case sensitive contains): : 1
define value for property 'groupid': : net.serenitybdd.tutorials
define value for property 'artifactid': : todomvctests
define value for property 'version': 1.0-snapshot: : 1.0.0-snapshot
define value for property 'package': net.serenitybdd.tutorials: :
confirm properties configuration:
groupid: net.serenitybdd.tutorials
artifactid: todomvctests
version: 1.0.0-snapshot
package: net.serenitybdd.tutorials
y: : y
maven will now generate a project skeleton for you:
y: : y
[info] ----------------------------------------------------------------------------
[info] using following parameters for creating project from archetype: serenity-junit-screenplay-archetype:1.1.19
[info] ----------------------------------------------------------------------------
[info] parameter: groupid, value: net.serenitybdd.tutorials
[info] parameter: artifactid, value: todomvctests
[info] parameter: version, value: 1.0.0-snapshot
[info] parameter: package, value: net.serenitybdd.tutorials
[info] parameter: packageinpathformat, value: net/serenitybdd/tutorials
[info] parameter: package, value: net.serenitybdd.tutorials
[info] parameter: version, value: 1.0.0-snapshot
[info] parameter: groupid, value: net.serenitybdd.tutorials
[info] parameter: artifactid, value: todomvctests
[info] project created from archetype in dir: /users/john/projects/opensource/serenity/serenity-articles/screenplay-tutorial/sample-code/screenplay-tutorial
[info] ------------------------------------------------------------------------
[info] build success
[info] ------------------------------------------------------------------------
[info] total time: 04:52 min
[info] finished at: 2016-02-15t09:16:05+00:00
[info] final memory: 16m/309m
[info] ------------------------------------------------------------------------
you will find your new project in the
screenplay-tutorial
directory.
$ cd screenplay-tutorial
$ mvn clean verify
or, if you prefer gradle, run the following:
$ gradle test aggregate
both will run a simple test performing a search on google and generate some reports in the
target/site/serenity
directory. open the
index.html
file in this directory to take a look.
3. serenity screenplay — a quick tour
before we write some tests, let’s take a quick look at the sample code. import the project you just created into your favorite ide (we prefer intellij) and take a look at the
searchbykeywordstory.java
class. this should look something like this:
@runwith(serenityrunner.class)
public class searchbykeywordstory {
actor anna = actor.named("anna"); (1)
@managed(uniquesession = true)(2)
public webdriver herbrowser;
@steps
opentheapplication opentheapplication;
@before
public void annacanbrowsetheweb() {
anna.can(browsetheweb.with(herbrowser));
}
@test
public void search_results_should_show_the_search_term_in_the_title() {
giventhat(anna).wasableto(opentheapplication);
when(anna).attemptsto(search.fortheterm("bdd in action")); (3)
then(anna).should(eventually(seethat(thewebpage.title(), (4)
containsstring("bdd in action"))));
}
}
(1) anna is the main actor in our scenario |
(2) anna can access the application via a web browser |
(3) actor can perform tasks, such as searching for a particular term |
(4) actors can check the state of the application, for example by checking what the page title should display. |
let’s go through this test to get an idea of how a typical serenity screenplay test is built. screenplay tests are expressed from the point of view of one or more actors . actors have abilities , such as the ability to browse the web using a browser. actors perform business-focused tasks to achieve their goals, such as "search for a term". actors can also ask questions about the state of the application, such as checking the state of the result screen.
figure 1. the screenplay pattern is built around actors who use their abilities to perform tasks and ask questions about the state of the system in order to achieve their business goals.
when you run this test, either through maven/gradle or from the command line, it will produce a rendered version of the test in html that looks something like this:
now that you’ve seen what a typical screenplay test looks like, lets see just how easy they are to write.
4. your first serenity screenplay test
serenity screenplay adds a highly readable dsl to structure and express your tests in terms of business tasks. to see how this dsl in action, we are going to write some acceptance tests for the dojo implementation of the todomvc application (see http://todomvc.com/examples/dojo/ ).
figure 2. the todomvn application
the first test we will write will simply check that when you add a new todo item to the list, it appears in the list.
start off by creating a new package called
record_items
under the
features
package. this will represent the application
capability
to record todo items. inside this package, create a new test class called
additemsstory
like the following:
@runwith(serenityrunner.class) (1)
public class additemsstory {
}
(1) tells junit that this is a serenity test |
next, we will add an actor to our scenario. we’ll call our actor justin. add the following line to your class to cast justin as an actor in our scenario:
actor justin = actor.named("justin"); (1)
(1) cast a new actor in the scenario called justin. |
now in this scenario we are testing a web application, so we need to give justin a browser to use. (other tests might need other abilities, such as the ability to query a web service or a database). serenity manages the webdriver lifecycle for us - all we need to do is to declare a variable for the browser in the test, and assign it to our actor:
@managed
public webdriver hisbrowser; (1)
@before
public void justincanbrowsetheweb() {
justin.can(browsetheweb.with(hisbrowser)); (2)
}
(1) this webdriver instance will be automatically instantiated and shut down by serenity |
(2) whenever justin accesses the web, he will use this browser |
now we can write our first test. the aim of the test is to add a new item to the todo list, and verify that it appears in the list of items below. so we could write something like this.
@test
public void should_be_able_to_add_an_item_to_the_todo_list() {
giventhat(justin).wasableto(startwith.anemptytodolist());
when(justin).attemptsto(addatodoitem.called("feed the cat"));
then(justin).should(seethat(thetodoitems.displayed(), hasitem("feed the cat")));
}
this is certainly easy enough to read, but the most important classes (
startwith
,
addatodoitem
, and
thetodoitems
) exist for now only in our imagination. in fact, we are "writing the code we would like to have", and then implementing the classes for the tasks that we don’t already have. fortunately, filling in the gaps is not difficult, and after a little practice, it becomes very natural. let’s break this code down a little.
4.1. given/when/then
the code shown here uses words like "given", "when" and "then" to make the intent of the test more obvious. the
giventhat()
,
when()
and
then()
are static methods imported from the
net.serenitybdd.screenplay.givenwhenthen
class. if your ide doesn’t take care of this automatically for you, you can add the imports by hand as shown here:
import static net.serenitybdd.screenplay.givenwhenthen.*;
these methods are actually optional, and in some cases it makes sense to omit them entirely. for example, you can also write the second line shown above like this:
justin.attemptsto(addatodoitem.called("feed the cat"));
4.2. business tasks
serenity screenplay uses layers of abstraction to make tests more readable and more maintainable. serenity describes how a user interacts with an application in terms of three layers:
- goals that represent the high level business objectives;
- tasks that describe the high-level steps the user takes to achieve these goals; and
- actions that describe how the user interacts with the application to perform each step.
the
goal
is represented by the test or scenario name (
should_be_able_to_add_an_item_to_the_todo_list()
for this test).
tasks
are represented by classes, such as
startwith
and
addatodoitem
in this test. we use readable class and method names that use domain language to make the tests as readable as possible.
4.2.1. implementing a simple business task class
task classes are easy to write. let’s start off with
startwith
:
giventhat(justin).wasableto(startwith.anemptytodolist());
create a new class called
startwith
in the
tasks
package, and make it implement the
net.serenitybdd.screenplay.task
interface. this interface implements a single method,
performas()
, which is where the action happens:
public class startwith implements task {
@override
public <t extends actor> void performas(t actor) {
}
}
the actor methods
attemptsto()
and
wasableto()
take a list of
task
objects and successively call the
performas()
method for each task. we use static builder methods such as
startwith.anemptytodolist()
to prepare the
task
objects before they are executed, and pass in any variables the task may need. this helps make the code read more fluently. in this simple case, we don’t need to do anything special, so the
anemptytodolist()
static method just returns an instance the
startwith
class. the only thing we do need to do is to add some serenity instrumentation to the instance using the
net.serenitybdd.screenplay.tasks.instrumented()
method, so that the task and underlying actions will appear in the serenity reports:
public static startwith anemptytodolist() {
return instrumented(startwith.class);
}
now let’s come back to the
performas()
method. an actor performs a task by either performing other smaller tasks or by interacting with the application in some way. in the case of the
startwith
task, we just want to open the todomvc application. the implementation looks like this:
todomvcapplicationhomepage todomvcapplicationhomepage; (1)
@override
@step("{0} starts with an empty todo list") (2)
public <t extends actor> void performas(t actor) {
actor.attemptsto(
open.browseron().the(todomvcapplicationhomepage) (3)
);
}
(1) declare a page object that serenity will automatically instantiate |
what should this step look like in the test reports ({0} will be replaced with the name of the actor) |
the actor performs this task simply by opening the web browser on the todomvc application homepage. |
the
@step
annotation tells serenity how this step should be written in the test reports. the
{0}
expression represents the actor variable that is passed into the
performas()
method. we will see later how we can personalize this message further.
4.2.2. using action classes
we interact with the web application using the
open
action class.
action
classes are like
task
classes, except that they interact with the application directly and are called from within a task, not directly from the test. serenity comes with a number of built-in ui-related interaction classes to help interact with web pages, including
open
,
click
,
enter
,
hit
,
select
and
scroll
. the
open
class opens the actor’s browser to the url of a particular page, as shown here:
todomvcapplicationhomepage todomvcapplicationhomepage;
...
actor.attemptsto(
open.browseron().the(todomvcapplicationhomepage)
);
the page is represented by the
todomvcapplication
class. this is a simple serenity page object, that at this stage needs nothing more than a
@defaulturl
annotation to indicate what url should be used when we open the browser on this page:
@defaulturl("http://todomvc.com/examples/dojo/")
public class todomvcapplicationhomepage extends pageobject {
}
4.2.3. implementing more complicated classes
we have now completed the implementation of the first step in the test. let’s move on to the second:
when(justin).attemptsto(addatodoitem.called("feed the cat"));
here, we have a task called
addatodoitem
, which will add a todo item to our todo list. create a class called
addatodoitem
, once again in the
tasks
package, along the following lines:
public class addatodoitem implements task {
private final string itemname; (1)
@step("{0} adds an item called '#itemname'") (2)
@override
public <t extends actor> void performas(t actor) {
// todo
}
public addatodoitem(string itemname) { (3)
this.itemname = itemname;
}
public static task called(string itemname) {
return instrumented.instanceof(addatodoitem.class) (4)
.withproperties(itemname);
}
}
(1) the itemname field stores the name of the todo item we want to add |
(2) we can refer to member variables like itemname in the @step annotation using the # sign |
(3) we initialize the member variable in the constructor |
(4) create an instrumented instance of the addatodoitem and pass the itemname argument to the addatodoitem constructor |
this class shows a more flexible type of
task
, where we use a static method call (such as
addatodoitem.called("feed the cat")
) to create an instance of the task with a particular value. we will be able to use this value in the
performas()
implementation later on. we still need to instrument the class instance, but this time we use the
instrumented.instanceof()
method to pass a parameter to the constructor.
as in the
displayatodolistwith
task, all the interesting stuff happens in the
performas()
method. here, we lay out the actions that the actor needs to do to add a todo item to the list. adding a todo item is relatively straight-forward. the actor types the name of the todo item into the input field and hits the return key. in the
performas()
method, we can write something very similar:
@step("{0} adds an item called '#itemname'")
@override
public <t extends actor> void performas(t actor) {
actor.attemptsto(
enter.thevalue(itemname).into(todolist.what_needs_to_be_done)
.thenhit(keys.return)
);
}
here we are using one of the serenity ui interaction classes (
enter
) to enter a value into a given field, and then press the return key. we do need to tell serenity how to find the "what needs to be done" field, and we do this in the
todolist
class. the
todolist
class is responsible for knowing how to locate elements in the todo list, and looks like this:
import net.serenitybdd.screenplay.targets.target;
public class thetodolist {
public static target what_needs_to_be_done
= target.the("'what needs to be done?' field").locatedby("#new-todo");
}
the
target
class is a convenient way to associate a user-readable text ("what needs to be done") with a webdriver location strategy. this way, it is the text that appears in the reports, and not the css selector, which makes the intent of the test clearer.
4.3. asking questions
the final step in our test is to check whether the todo item has actually appeared in the todo list. once again, we consider the problem from the point of view of the actor — what would justin expect to see, to know that the action has been successful? if we had to explain to a new tester what she should check for, we might say something like "you should see that the feed the cat item now appears in the list of todo items", or "you should see that the todo items that are displayed now contains feed the cat ".
in serenity, we use the
actor
's
should()
method to write something very similar:
then(justin).should(seethat(thetodoitems.displayed()), hasitem("feed the cat"));
the
should()
method takes a list of
consequences
that we expect to be true. you create a
consequence
using the
givenwhenthen.seethat
static method, which takes two parameters:
-
a
question
about the state of the application, and - a hamcrest matcher
we will see how to implement a
question
shortly, but in a nutshell, the question returns a value about the state of the application, and the hamcrest matcher describes what we expect this value to be.
the full test now look something like this:
import static net.serenitybdd.screenplay.givenwhenthen.*;
import static org.hamcrest.matchers.hasitems;
@test
public void should_be_able_to_add_an_item_to_the_todo_list() {
giventhat(justin).wasableto(startwith.anemptytodolist());
when(justin).attemptsto(addatodoitem.called("feed the cat"));
then(justin).should(seethat(thetodoitems.displayed(), hasitem("feed the cat")));
}
now all that remains is to implement the
thetodoitems
class.
4.3.1. implementing a question class
a question object answers a question about the state of the application, such as "what items are displayed in the todo list". questions implement the parameterized
question
interface. create a new class called
thetodoitems
in a
questions
package (next to the
tasks
package), along the following lines:
public class thetodoitems implements question<list<string>> { (1)
@override
public list<string> answeredby(actor actor) { (2)
return null; // todo
}
public static question<list<string>> displayed() { (3)
return new thetodoitems();
}
}
(1) question classes implement the question interface |
(2) we return the answer to the question in the answeredby() method |
(3) a convenient static method used to create a new question instance |
now all that remains is to implement the
answeredby()
method. if we need to, we can access the actor’s browser directly by calling
browsetheweb.as(actor)
, as shown here:
list<webelement> itemlabels = browsetheweb.as(actor)
.findall(by.cssselector(".view label"));
however serenity also provides a set of classes that can help query a web page more smoothly, and take care of boiler-plate code such as type conversions and list processing.
public list<string> answeredby(actor actor) {
return text.of(todolist.items) (1)
.viewedby(actor) (2)
.aslist(); (3)
}
(1) return the list of text values from elements matching this locator target |
(2) using the actor’s browser |
(3) and and convert them to a list of strings |
the last piece of the puzzle is to add the
items
locator target to our
todolist
:
public class todolist {
public static target what_needs_to_be_done
= target.the("'what needs to be done?' field")
.locatedby("#new-todo");
public static target items
= target.the("list of todo items")
.locatedby(".view label");
}
we now should have a fully working test that produces a nice readable report like this one:
5. reusing tasks — adding another test
serenity screenplay is designed to make tasks easy to reuse, and to make individual tasks as stable and reliable as possible. one way we do this is to make the tasks as small and focused as possible (conforming to the single responsibility principle ).
let’s see this idea in action. suppose, for example, we wanted to add another test to check that we can add new todo items to an existing list. the test might look like this:
@test
public void should_be_able_to_add_additional_todo_items() {
giventhat(justin).wasableto(
startwith.atodolistcontaining("feed the cat","take out the garbage")
);
when(justin).attemptsto(addatodoitem.called("walk the dog"));
then(justin).should(seethat(thetodoitems.displayed(),
contains("feed the cat","take out the garbage","walk the dog")));
}
as you can see, this test reuses the existing tasks like
start
and
addatodoitem
extensively: in fact, the only change we need to make is to add the
atodolistcontaining()
method to the start class. we could make this change by modifying the logic in the current
startwith
class, but this would add complexity to the existing class and risk affecting tests other than the one we are currently working on.
an alternative approach would be to create a new task dedicated to preparing a todo list with prepopulated items. this way, our original task remains untouched, and we can focus on adding a new, less complicated task implementation.
let’s start by duplicating the existing
startwith
class to a class called
startwithanemptylist
, and remove the static factory method from the new class:
public class startwithanemptylist implements task {
todomvcapplicationhomepage todomvcapplicationhomepage;
@override
@step("{0} starts with an empty todo list")
public <t extends actor> void performas(t actor) {
actor.attemptsto(
open.browseron().the(todomvcapplicationhomepage)
);
}
}
next, we will refactor the
startwith
to act as a factory class, so that the
anemptytodolist()
method returns an instrumented instance of the
startwithanemptylist
class:
public class startwith {
public static startwithanemptylist anemptytodolist() {
return instrumented(startwithanemptylist.class);
}
}
the behavior of the first test should not have been altered by this change (though you should rerun it just to be sure).
now let’s add the
atodolistcontaining()
method to the
startwith
class:
import com.google.common.collect.lists;
import net.serenitybdd.core.steps.instrumented;
import static net.serenitybdd.screenplay.tasks.instrumented;
public class startwith {
public static startwithanemptylist anemptytodolist() {
return instrumented(startwithanemptylist.class);
}
public static startwithatodolistcontaining atodolistcontaining(string... todos) {
return instrumented.instanceof(startwithatodolistcontaining.class)
.withproperties(lists.newarraylist(todos));
}
}
now, all we need to do is to implement the
startwithatodolistcontaining
class. a simple implementation might look like this:
public class startwithatodolistcontaining implements task {
private final list<string> todos;
public startwithatodolistcontaining(list<string> todos) {
this.todos = todos;
}
@override
@step("{0} starts with a todo list containing #todos") (1)
public <t extends actor> void performas(t actor) {
actor.attemptsto(startwith.anemptytodolist()); (2)
todos.foreach(
todoitem -> actor.attemptsto(addatodoitem.called(todoitem)) (3)
);
}
}
we can refer to member variables in the @step annotation using the hash sign |
start with an empty todo list |
add each specified item to the list |
this will produce a report like this:
6. conclusion
hopefully you will now know enough to get started with serenity screenplay, and understand both the mechanics of using the pattern, as well as some of the ideas behind it. serenity screenplay has many additional features that we didn’t have time to go into here: learn more on the serenity bdd site and in the user’s manual .
Opinions expressed by DZone contributors are their own.
Comments