The Build File
As I mentioned, I’m going to go through this example using Gradle. Create a new directory called serenity-screenplay. This will be our project root. Go into that directory and use this command:gradle initThat will do all you need to get a Gradle project set up. You can now import that project directory into your IDE of choice although, as I said before, I don’t really assume any particular IDE. That being said, as you’ll come to see, using the screenplay pattern will likely be a lot easier if you have an IDE. Once you have everything set up the way you want it, edit the build.gradle file so that it looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
apply plugin: 'java' apply plugin: 'net.serenity-bdd.aggregator' repositories { mavenLocal() jcenter() } buildscript { project.ext { serenityVersion = '1.1+' } repositories { mavenLocal() jcenter() } dependencies { classpath('net.serenity-bdd:serenity-gradle-plugin:' + serenityVersion) } } dependencies { compile 'net.serenity-bdd:serenity-core:' + serenityVersion compile 'net.serenity-bdd:serenity-journey:' + serenityVersion compile 'net.serenity-bdd:browse-the-web:' + serenityVersion testCompile 'junit:junit:4.12' testCompile 'net.serenity-bdd:serenity-junit:' + serenityVersion testCompile 'net.serenity-bdd:serenity-cucumber:' + serenityVersion testCompile 'net.serenity-bdd:serenity-jbehave:' + serenityVersion testCompile 'org.assertj:assertj-core:3.4+' testCompile 'org.slf4j:slf4j-simple:1.7+' } gradle.startParameter.continueOnFailure = true |
The Application Under Test
Since I’m following much of the existing documentation out there, I’ll use the traditional TodoMVC application by way of example. There are different implementations for different JavaScript libraries, such as one for Dojo and another for AngularJS. The common tutorial usually starts off simple, adding a new todo item to a list. I’ll continue that trend here.Creating the Features Package
The screenplay pattern is mainly designed with the idea of code constructs being used to build a readable API of methods and objects. This API is used to express your acceptance criteria as tests, while keeping the business terminology in place. However, in training people on this framework, I’ve found that getting into the appropriate cadence for this can be kind of tricky, particularly as you are learning to structure the packages that you want. Once that structure is in place, the learning curve sharply declines. Assuming you applied the Gradle or Maven build, you should have a standard package/directory structure withsrc/test/java
and src/main/java
as roots. In the src/test/java package, create a package called:
com.testerstories.tutorial.todos.features.add_todoYou can actually call the domain portion whatever you want; obviously I used my own site for example purposes. What I want to call out here is the “todos.features.add_todo” part. This is not required by Serenity but what I did was add the name of the app I’m testing (todos) which could also stand in for the overall business ability being demonstrated by the tests I’ll write. Then I have a structuring location (features) and then the specific feature (add_todo). What this is doing is representing the application capability to add todo items. Again, Java packaging is one of those things that teams decide on their own. I’m just showing you an approach that you will commonly see, particularly in regards to Serenity.
Establish the Test Runner
In that package you just made, create a class called AddNewTodo, which is the file AddNewTodo.java. From this point forward I’ll assume you know that the class name matches the file name in Java and won’t state it every time. Let’s establish right away that this class is going to be run via Serenity’s JUnit runner:
1 2 3 4 5 6 7 8 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import org.junit.runner.RunWith; @RunWith(SerenityRunner.class) public class AddNewTodo { } |
Actors
Now we need to add an actor to our scenario. Given the name screenplay pattern, it probably won’t surprise you that an actor is a key concept. This is very similar to how a persona was a key concept in the journey pattern. In Serenity terms, what you’re doing here is creating a persona that is being used to understand a specific role. An actor is used to “play the role” and their job is to perform a task, or set of tasks, in a scenario. I suppose if you wanted to continue the analogy you could say that a scenario is sort of like a scene in a screenplay. Let’s first just focus on getting our actor in place.
1 2 3 4 5 6 7 8 9 10 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import net.serenitybdd.screenplay.Actor; import org.junit.runner.RunWith; @RunWith(SerenityRunner.class) public class AddNewTodo { Actor jeff = Actor.named("Jeff"); } |
Manage the Driver
I’m going to be testing a web application so I need to give Jeff a browser to use. Hey, look at that! See how easy it is to fall into speaking as the user? Imagine constructing this kind of automation as you are having discussions with the business. This may seem like nothing more than an affectation but in the ever increasingly fast-paced world of development cycles, the ability to have conversations directly translated into working code is something that I think is very much worth investigating. Anyway, back on topic: you can use Serenity to manage the lifecycle of certain aspects. For testing web applications this would be managing the WebDriver lifecycle. So let’s get that in place.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import net.serenitybdd.screenplay.Actor; import net.thucydides.core.annotations.Managed; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; @RunWith(SerenityRunner.class) public class AddNewTodo { Actor jeff = Actor.named("Jeff"); @Managed(driver = "chrome") WebDriver theBrowser; } |
1 2 |
webdriver.driver=chrome webdriver.chrome.driver = path_to_chromedriver |
brew install chromedriver
which takes care of all this for you. Also I should note that if you use the first line in the serenity.properties file as I just showed it, you do not need to add the driver = "chrome"
part to the @Managed annotation.
Going back to the above code, What this code says is that whatever is stored by the variable theBrowser is managed. In the case, theBrowser will be an instance of WebDriver and so what that means is the WebDriver instance will be automatically instantiated and shut down by Serenity, removing the need for you to handle this as part of your overall test logic. Further, whenever the user Jeff has a test that has him accessing the web, he will use that managed browser.
User Drives the Browser
Actors need to be able to do things to perform their assigned tasks. So we give our actors abilities. One relatively mundane ability that Jeff must have is the ability to browse the web using a browser. So now we need to assign this browser to our actor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.abilities.BrowseTheWeb; import net.thucydides.core.annotations.Managed; import org.junit.Before; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; @RunWith(SerenityRunner.class) public class AddNewTodo { Actor jeff = Actor.named("Jeff"); @Managed(driver = "chrome") WebDriver theBrowser; @Before public void jeffCanBrowseTheWeb() { jeff.can(BrowseTheWeb.with(theBrowser)); } } |
jeff
actor instance is given an ability to do so and the ability class handles those details.
This is an important design principle in that it keeps the things an actor can do separate from the actor itself. This makes it possible to avoid changing the actor but instead extend what the actor can do by creating abilities and associating those abilities with the actor instance.
Putting this particular statement in the @Before method makes it clear that this is a precondition for any tests. You can, if you prefer, use even a little more syntactic sugar. Consider this slight modification:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.abilities.BrowseTheWeb; import net.thucydides.core.annotations.Managed; import org.junit.Before; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import static net.serenitybdd.screenplay.GivenWhenThen.givenThat; @RunWith(SerenityRunner.class) public class AddNewTodo { Actor jeff = Actor.named("Jeff"); @Managed(driver = "chrome") WebDriver theBrowser; @Before public void jeffCanBrowseTheWeb() { givenThat(jeff).can(BrowseTheWeb.with(theBrowser)); } } |
Provide a Test
Now we can add a test. The aim of this first test will be to add the first item to a todo list. We’re not going to implement the full test in this post, but we can at least get the start of it going. So add the following:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 |
package com.testerstories.tutorial.todos.features.add_todo; import net.serenitybdd.junit.runners.SerenityRunner; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.abilities.BrowseTheWeb; import net.thucydides.core.annotations.Managed; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.openqa.selenium.WebDriver; import static net.serenitybdd.screenplay.GivenWhenThen.givenThat; @RunWith(SerenityRunner.class) public class AddNewTodo { Actor jeff = Actor.named("Jeff"); @Managed(driver = "chrome") WebDriver theBrowser; @Before public void jeffCanBrowseTheWeb() { givenThat(jeff).can(BrowseTheWeb.with(theBrowser)); } @Test public void should_be_able_to_add_the_first_todo_item() { } } |
Constructing the Test DSL
As I go forward here one thing needs to be made clear: Serenity and the screenplay pattern very much treat development in a test-first fashion. This means you’ll often be writing methods on classes as part of your tests. And until your test DSL starts to build up, neither the classes nor the methods will exist at first. You follow the practice of writing the code you would like to have, meaning you write the code as you want it to read. Then you create the classes and implement the methods for what doesn’t exist. Keep that in mind as we consider this next bit.Layers of Abstraction
Screenplay patterns use layers of abstraction to make tests more readable and ultimately more maintainable. This is an area of huge debate right now in the test community. But let’s consider the layers that Serenity puts in place for you.- Goals: These represent high-level business objectives.
- Tasks: These describe the high-level steps a user takes to achieve the goals.
- Actions: These describe the low-level user interactions with the application to perform the tasks.
should_be_able_to_add_the_first_todo_item
). The tasks are going to be represented by classes. The actions are also going to be represented by classes. There is a key distinction here. The methods on Action classes will be called from Task classes, never directly from the test. This is as opposed to tasks. Tasks are methods on classes that are called directly from a test.
Putting that a bit more clearly: actions are the implementation details. Those implementation details are ideally abstracted away from the tests. Tasks are business details. This means tests ideally only change if the tasks (i.e., business rules) change but do not have to change if it’s only the underlying implementation (the actions) that are changed.
We’re going to go through this in some detail but I want to make sure the concepts are clear first. Serenity throws a lot of stuff at you and it’s easy to get lost if you can’t see how the meat hangs on the skeleton, so to speak.
I should note that experienced automation engineers are already well aware of how to use layers of abstraction to separate the intent of the test from the implementation details. Layers of abstraction have long been used to separate the ‘what’ from the ‘how’. By way of my own example, you can see how in a particular Java example for a login test, I have a few @Test methods that showcase different ways of providing an, admittedly simplistic, abstraction layer. You can see something similar in my code for a planet weight test. Specifically, see the “non-fluent approach” starting on line 19 and the “fluent approach” on line 27.
Being able to define the appropriate level around layers of abstraction is one of the key skills of automation engineers. I’m certainly not the first, or the only, to say that. But I do most wholeheartedly believe it.
Models of Interaction and State
There’s one other point I want to bring up here and it has to do with modeling two very different aspects of implementation: the interaction and the state of what you are interacting with. This is one of those intersections of testing that I talked about. I don’t want us to get hung up on that right now, but let’s at least apply a little of that thinking here. Test scenarios have always been predicated on describing how a user interacts with the application to achieve a given goal. Frameworks like Serenity take this concept to heart and suggest that tests scenarios read as if they are presented from the point of view of the user. This would be as opposed to writing them from the “point of view” of pages, which is what the page object pattern has long promoted. You’ve already seen that we call a user interacting with the system an actor. Actors have certain abilities, such as browsing the web, that are absolutely necessary to do anything at all. Another example might be the ability of an actor to use a REST client to send a request to a web service. Beyond abilities, actors can also perform tasks. In the case of our working example here, a task would be adding an item to the todo list. To achieve these tasks, the actor will typically need to interact with the application. This mirrors what a real person would do: enter values into text fields, select items from lists, click on buttons, and so on. Those interactions are individual actions. All of what I’m saying here is about modeling interaction. But you can also model state. To this end, actors can also ask questions about the state of the application. This might mean reading a value from a text field on the screen, checking that some element was dragged to the appropriate place, making sure certain elements are enabled or disabled, and so on. The Serenity documentation uses the following diagram to show the actor-centric model in action: I think that is a fairly concise way to conceptualize the specific intersections of testing within the context of the Serenity framework. Let’s get back to some coding to reinforce some of this theory with the practice.Adding Tasks
We know that our test is about Jeff adding the first todo item — because of our scenario title — so that would imply that Jeff starts off with an empty list. Let’s start framing our test accordingly by writing down what we want to say.
1 2 3 4 |
@Test public void should_be_able_to_add_the_first_todo_item() { givenThat(jeff).wasAbleTo(StartWith.anEmptyTodoList()); } |
1 |
jeff.wasAbleTo(StartWith.anEmptyTodoList()); |
com.testerstories.tutorial.todos.tasksUnder this package is where we’re going to put the StartWith class. This class is going to implement the Task interface. Here’s what StartWith should look like initially:
1 2 3 4 5 6 |
package com.testerstories.tutorial.todos.tasks; import net.serenitybdd.screenplay.Task; public class StartWith implements Task { } |
1 2 3 4 5 6 7 8 9 10 11 |
package com.testerstories.tutorial.todos.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; public class StartWith implements Task { @Override public <T extends Actor> void performAs(T actor) { } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 |
package com.testerstories.tutorial.todos.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; public class StartWith implements Task { @Override public <T extends Actor> void performAs(T actor) { actor.attemptsTo( ); } } |
1 |
givenThat(jeff).wasAbleTo(StartWith.anEmptyTodoList); |
Adding Actions
Keeping in mind that StartWith is a task, within each task the performAs() method is where the instructions for the task are stored. These are the actions that are required to complete the task. So here I just need to open the browser to the appropriate page. Now, again, I’m going to be very deliberate about what I show you here. I realize this can seem like I’m plodding along. Bear with me. Let’s add this bit of logic:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.testerstories.tutorial.todos.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; public class StartWith implements Task { @Override public <T extends Actor> void performAs(T actor) { actor.attemptsTo( Open.browserOn().the(applicationHomePage); ); } } |
the()
method, when called on browserOn(), is looking for a page object. I’ll deal with that in a second but I want to point out here that it can seem like a lot of “magic” is happening behind the scenes. After all, I basically have a line of code that says:
1 |
actor.attemptsTo(Open.browserOn().the(applicationHomePage)) |
Adding Page Objects
Okay, so back to the applicationHomePage instance. What I need here is a class that is going to represent the page, the instance of which will be used in the Open action. So let’s add this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.testerstories.tutorial.todos.tasks; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; public class StartWith implements Task { ApplicationHomePage applicationHomePage; @Override public <T extends Actor> void performAs(T actor) { actor.attemptsTo( Open.browserOn().the(applicationHomePage) ); } } |
com.testerstories.tutorial.todos.uiNotice that the ui package and the tasks package sit at the same level. In the ui package create the class ApplicationHomePage. This class will extend the PageObject class and should initially look like this:
1 2 3 4 5 6 |
package com.testerstories.tutorial.todos.ui; import net.serenitybdd.core.pages.PageObject; public class ApplicationHomePage extends PageObject { } |
1 2 3 4 5 6 7 8 |
package com.testerstories.tutorial.todos.ui; import net.serenitybdd.core.pages.PageObject; import net.thucydides.core.annotations.DefaultUrl; @DefaultUrl("http://todomvc.com/examples/angularjs/") public class ApplicationHomePage extends PageObject { } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.testerstories.tutorial.todos.tasks; import com.testerstories.tutorial.todos.ui.ApplicationHomePage; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; public class StartWith implements Task { ApplicationHomePage applicationHomePage; @Override public <T extends Actor> void performAs(T actor) { actor.attemptsTo( Open.browserOn().the(applicationHomePage) ); } } |
Can I execute my test?
You might be thinking: “Wow, I feel like I’ve done a lot of coding at this point. It sure would be great to know I’m not wasting my time. Can I run this thing?” Not quite yet. At this point, you should still have an error in your AddNewTodo class. Specifically, the “anEmptyTodoList” part is not specified at all because the method has not been created. So let’s take care of that. This method is actually going to be what’s known as a static builder method. These kinds of methods are in adherence to something called the Builder pattern. In this case, builder methods can be used to configure Task objects before they are executed. A builder method can, for example, pass in any variables that the task may need. In this case, however, there’s really nothing for a builder method to provide for the StartWith task at the moment. We do have to create the method, of course, or we can’t compile. As a note, if you use your IDE to generate this method, you should note that the method will, by default, attempt to return a Performable object. In other words, the method will look like this:
1 2 3 |
public static Performable anEmptyTodoList() { return null; } |
1 2 3 |
public static StartWith anEmptyTodoList() { return null; } |
1 |
return StartWith.class; |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
package com.testerstories.tutorial.todos.tasks; import com.testerstories.tutorial.todos.ui.ApplicationHomePage; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; import static net.serenitybdd.screenplay.Tasks.instrumented; public class StartWith implements Task { ApplicationHomePage applicationHomePage; @Override public <T extends Actor> void performAs(T actor) { actor.attemptsTo( Open.browserOn().the(applicationHomePage) ); } public static StartWith anEmptyTodoList() { return instrumented(StartWith.class); } } |
Execute (Finally!)
Okay, moment of truth here. Try this:gradle clean test aggregateIf the programmatic gods grace you with their presence, this should execute the test, opening up a browser and navigating to the TodoMVC page, and then closing the browser again. Assuming all went well, open the following file in your browser: target/site/serenity/index.html.
The Test Report
On that index page, under the “Tests” section near the bottom, you should see a line that says:"Should be able to add the first todo item"That’s basically coming from the test method itself. That’s the goal. If you click on that line, you’ll see details of the test itself and you should note that the only line says:
"Jeff opens the Application home page"That’s the action that took place. But where did that specific wording come from? That’s essentially coming from the action steps stored in the attemptsTo() method of the StartWith task. Keep in mind the task does this:
1 2 3 |
actor.attemptsTo( Open.browserOn().the(applicationHomePage) ); |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
package com.testerstories.tutorial.todos.tasks; import com.testerstories.tutorial.todos.ui.ApplicationHomePage; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Task; import net.serenitybdd.screenplay.actions.Open; import net.thucydides.core.annotations.Step; import static net.serenitybdd.screenplay.Tasks.instrumented; public class StartWith implements Task { ApplicationHomePage applicationHomePage; @Override @Step("{0} starts with an empty todo list") public <T extends Actor> void performAs(T actor) { actor.attemptsTo( Open.browserOn().the(applicationHomePage) ); } public static StartWith anEmptyTodoList() { return instrumented(StartWith.class); } } |
Jeff, this is is brilliant.
I just read through your articles on screenplay. You have really made the concepts free from distracting things and found the right words for a screenplay beginner. Very comprehensible!
Keep up the work with your interests and your, from my perspective, gift of explaining things!
/Martin
Running the tests as described above I got this Exception:
net.thucydides.core.webdriver.UnsupportedDriverException: Could not instantiate class org.openqa.selenium.chrome.ChromeDriver
Adding these lines made the tests pass:
Excellent point! Thank you for bringing this to my attention. This certainly makes sense. I have ChromeDriver on my path all the time and so, of course, the code as I provided it worked. That was bias I became blind to. I am updating the article to make sure that it is clear the ChromeDriver must be available on your path or, alternatively, provide a path as you did.
Thank you again for bringing this to my attention as well as the attention of anyone else reading this.
Nice introduction. I like the concept of demonstrating and discussing an API by means of a focused example.
When following along however, I encountered an issue with the build configuration. The code compiled, but execution ended with a com.google.inject.ConfigurationException. The Serenity report said that an error outside of step execution occurred and that the interface WebdriverManager was not bound to an implementation.
It took me a while to figure it out, but it seems to be a dependency problem. The build.gradle file contains a dependency for browse-the-web. This was however renamed to serenity-screenplay-webdriver in Serenity 1.1.36-rc.1, so Gradle uses version 1.1.35, which depends on the same version of serenity-core.
But build.gradle also specifies 1.1+ as the version of most of the dependencies (including serenity-core). Currently this is version 1.1.37-rc.7. Because the Java classpath can contain only one version of a class, Gradle uses the highest version after dependency calculation.
Unfortunately in version 1.1.37 the instantiation of a WebdriverManager has changed and the class BrowseTheWeb in browse-the-web can no longer retrieve an instance through dependency injection.
The solution is to depend on serenity-screenplay-webdriver instead or use version 1.1.35 for most (but not all) of the dependencies. Also serenity-journey, serenity-cucumber, and serenity-jbehave can be removed regardless. The first was renamed to serenity-screenplay in version 1.1.26-rc.2. but this is already a dependency for browse-the-web or serenity-screenplay-webdriver. The last two are not used.
A convenient utility is ‘gradle dependencies –configuration=testRuntime’, which shows which calculated versions are actually used.
Excellent analysis, Ronald. I appreciate you posting your feedback here about that. I will attempt to modify this post to incorporate your feedback. Thanks again for this. I know it can be frustrating to go through something and have to work out a series of unplanned for issues.
Great article! When to expect part 2?
Already available. (I probably should post links in these kinds of series articles.)
Part 2
http://testerstories.com/2016/06/screenplay-pattern-with-java-part-2/
Part 3
http://testerstories.com/2016/07/screenplay-pattern-with-java-part-3/
Excelent article, good work and I’m really glad I stumbled upon this.
Can you please elaborate more on the user abilities and how to write custom abilities that implement the Ability interface?
Reading this article was like listening to one of my favorite songs. I can’t wait to read the next one. Thank you very much, Jeff!
Why is there no Gherkin in this example?
This was a post to show the Screenplay pattern which does not necessarily have to use Gherkin. In the article I state:
“Incidentally, if you are using this approach with Cucumber, you could certainly leave out the Given/When/Then methods as the intent is generally explicit in the Cucumber steps because Cucumber relies on Gherkin.”
That being said, there is an internal Gherkin being used here. If you follow the series of posts, you end up with code like this:
That is an example of pushing English up (Gherkin internal) rather than pulling English down (Gherkin external).
Change the dependency ‘net.serenity-bdd:browse-the-web’ to ‘net.serenity-bdd:serenity-screenplay-webdriver’ in build.gradle as this dependency has been renamed.