This post follows on from the previous (see
part 1 and
part 2). Here we’ll wrap up the test we started with by making sure the test actually performs some sort of verification. This will complete my initial tour of screenplay-based functionality within the context of Serenity.
At this point, we’ve done a lot of coding and setting up our test. But we don’t actually have a test because we don’t check anything! So now let’s go back to our test class — AddNewTodo — and figure out what we want to check for to say that this test has been successful.
Going back to the domain model we’ve been building up, as with all test steps, it’s important that we consider the tasks from the point of view of the actor. In this case, what would Jeff expect to see so that he knew that the action he took was successful? Think of how you might explain it to a person: “Jeff should see that the displayed todo items list has a list item called ‘Digitize JLA vol 1 collection’.” So let’s translate that basic statement into code. Add the following line to your test method (and I’m showing the relevant imports as well here):
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
|
package com.testerstories.tutorial.todos.features.add_todo; import com.testerstories.tutorial.todos.tasks.AddATodoItem; import com.testerstories.tutorial.todos.tasks.StartWith; 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.*; import static org.hamcrest.CoreMatchers.hasItem; @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() { givenThat(jeff).wasAbleTo(StartWith.anEmptyTodoList()); when(jeff).attemptsTo(AddATodoItem.called("Digitize JLA vol 1 collection")); then(jeff).should(seeThat(TodoItemsList.displayed(), hasItem("Digitize JLA vol 1 collection"))); } } |
Here I’m finally using the final of the triumvirate: a then() method to go along with the given and when methods used previously. I’ll quickly note here that the seeThat() method is provided as part of the GivenWhenThen class. Your IDE will likely wrap up the imports at this point, as I’m showing in line 14.
The one thing that will be unresolved at this point is the TodoItemsList. Before we worry about implementing that, let’s take a step back and unwrap a bit of what is going on with our newly added test line.
The should() method, which is being called on an Actor instance, takes a list of Consequence objects. These are objects that you expect to return true. You create a Consequence using the GivenWhenThen.seeThat() static method.
That method is taking two parameters. It takes a Question object, which should be an object that is checking some aspect of state on the application, and a Matcher object which effectively serves as an assertion or expectation. Just take a moment to make sure you see how this bit of code is hanging together. Our Question should return a value representing some aspect of state and our Matcher should describe what we expect that value to be.
In this particular case, you’ll note that I’m using a Hamcrest matcher, but you can certainly use any assertion library you prefer. AssertJ, for example, is a very good one.
The idea of a Consequence class is deliberate in terms of the naming. The idea is that there is an expected consequence as a result of a scenario being carried out. This consequence is placed in the “viewpoint” (code context) of the actor via the should() method.
Questions
So let’s start implementing some of these ideas in code by creating our TodoItemsList. Let’s create a new package called
questions
. This will be in the src/main/java root package and will sit at the same level as the tasks and ui packages.
Let’s have this Question class implement the Question interface. A Question object must provide a static method called answeredBy() and your IDE will likely prod you to include this. So to start off with your class will look like this:
|
package com.testerstories.tutorial.todos.questions; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Question; public class TodoItemsList implements Question { @Override public Object answeredBy(Actor actor) { return null; } } |
Looking at this code you might recognize that this is somewhat similar, from a code perspective, to what we did when we handled tasks. In fact, Question objects are similar to Task and Action objects. One major difference, of course, is that instead of the performAs() method used for Tasks and Actions, a Question class needs to implement the answeredBy() method.
A few things to note here is that the answeredBy() method is returning a generic Object. We’ll want to change that. Also, we most certainly don’t want to return null. To understand what we want to return, let’s consider what we’re expecting to happen in this method. Basically, we want to return the list of text values from elements that match the todo list. We’re doing that because ultimately we want to check if the list item we entered shows up in that list. So that brings up a couple of implementation details: I need to be able to locate the todo list, then I need to be able to get that list into a format that I can check.
One thing to note is that the Question interface expects to be bounded by a type. That type should contain the “answer type” that you expect to be returned when the question is answered. In this case I’m going to be getting a list of text items from the web page. Or, rather, I plan on getting a bunch of text from a particular element on the page. I want to store that text in a list of strings. So let’s add this to our TodoItemsList class:
|
package com.testerstories.tutorial.todos.questions; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Question; import java.util.List; public class TodoItemsList implements Question<List<String>> { @Override public List<String> answeredBy(Actor actor) { return null; } } |
Here I’m bounding the parameter of the Question interface by saying that the type of answer it will be dealing with is a list of strings. Further note that I’m also changing the return type of answeredBy() from a generic Object to a list of string. Before dealing with the return statement, let’s also add the displayed() static method that we call from the test then() statement. If you auto-generate this method via your IDE you will likely see an interesting signature:
|
public static Question<? extends T> displayed() { return null; } |
What we want to is return a Question object bounded by the “answer type.” So change that bit of code to this:
|
public static Question<List<String>> displayed() { return null; } |
Great, so now we have two methods returning null! Okay, let’s stick with displayed() for a second. This is a static method that we are going to use to create a Question instance. What Question instance? Well, an instance of the class we’re in right now. Change the return statement of the displayed() method to this:
|
public static Question<List<String>> displayed() { return new TodoItemsList(); } |
So now we have our displayed() method return a new instance of the Question object but our question object itself is still returning null via the answeredBy() method. What we want that to do is return the list from the TodoMVC app page. Let’s change our answeredBy() method like this:
|
@Override public List<String> answeredBy(Actor actor) { return Text.of(".view label"); } |
This will require an import of “import net.serenitybdd.screenplay.questions.Text”. But what is that?
Serenity provides a number of interaction classes that let you query the web page using a fluent API. These interaction classes are used to wrap WebDriver calls that are used to query the web browser about the state of the page or objects within the page. Here the Text class does just what it sounds like: it gets the text that is contained by a given element.
Here I’m using the Text class. As you saw with the Action classes (like Open and Enter), Serenity provides a set of Question classes that can help query a web page for certain aspects of state. Here what I was attempting to do was return the text of a locator specified by “.view label”. You’ll find this won’t compile, however, and that’s because the .of() method is meant to return a target. Well, we already saw targets, right? Let’s add the following target to our TodoList class:
|
package com.testerstories.tutorial.todos.ui; import net.serenitybdd.core.pages.PageObject; import net.serenitybdd.screenplay.targets.Target; public class TodoList extends PageObject { public static Target WHAT_NEEDS_TO_BE_DONE = Target .the("'What needs to be done?' field") .locatedBy("#new-todo"); public static Target LIST_ITEMS = Target .the("List of todo items") .locatedBy(".view label"); } |
Now let’s use that in our .of() method:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
|
package com.testerstories.tutorial.todos.questions; import net.serenitybdd.screenplay.Actor; import net.serenitybdd.screenplay.Question; import net.serenitybdd.screenplay.questions.Text; import java.util.List; import static com.testerstories.tutorial.todos.ui.TodoList.LIST_ITEMS; public class TodoItemsList implements Question<List<String>> { @Override public List<String> answeredBy(Actor actor) { return Text.of(LIST_ITEMS); } public static Question<List<String>> displayed() { return new TodoItemsList(); } } |
I performed the static import just to make the code read a little clearer.
Is this enough? Clearly not because you’ll find that your return statement cannot compile. Why not? Keep in mind that answeredBy() should be returning a list of string. However, the call to Text.of() is not returning such a type.
One thing we have to specify, when asking a question, is who is asking the question or, rather, who the result of the question is being asked in the context of. A viewedBy() method is provided for this. We can pass the actor instance into this method. So add this refinement:
|
@Override public List<String> answeredBy(Actor actor) { return Text.of(LIST_ITEMS) .viewedBy(actor); } |
This still won’t compile because you still aren’t returning a list. A final refinement is to then add a toList() method to that. So let’s refine our answeredBy() method one more time:
|
@Override public List<String> answeredBy(Actor actor) { return Text.of(LIST_ITEMS) .viewedBy(actor) .asList(); } |
So here we return the list of text values from elements matching the locator target and this is being done in the context of the actor. Those text values are then converted to a list of strings.
Execute the Test
Let’s run our test:
gradle clean test aggregate
You should see that the test passes. You should find that it does. If you check the output in the report, you’ll see a line like this:
Then todo items list should be a collection containing 'Digitize JLA vol 1 collection'
To prove the test is actually passing, though, make sure you change then then() statement in your AddNewTodo to this:
|
then(jeff).should(seeThat(TodoItemsList.displayed(), hasItem("testing"))); |
Now when you run you will get an error. If you view the stack trace you will see message like this:
Expected: a collection containing "testing" but: was "Digitize JLA vol 1 collection"
Okay, so that’s good. Make sure you change your test statement back to what it was. What this proves it that you have a working test and, further you can get an appropriate indication of when a test failed.
Recap of Material
This seems like a good place to end this particular series of tutorials. We were able to look at many of the core classes of Serenity in terms of how those classes are used to implement the screenplay pattern.
So let’s recap a few points that hopefully came out of this series of posts.
One is the Goal / Tasks / Actions distinction. As you’ve seen the structure of this idea is maintained by conventions within Serenity, where actors are called upon in a goal (a test scenario, in a method designated by @Test). That scenario makes declarative calls to tasks, which implement a performsAs() method to execute the tasks. That in turn leads to actions, which are performed via an attemptsTo() method that is called in the context of the actor.
Beyond the matching of the domain to the code, this distinction matches that found in User-Centered Design (UCD) as well as Human Computer Interface (HCI) design, centered around the idea of Task Analysis. The three layers of detail in those disciplines is also goal, task, action.
My focus here was clearly on the screenplay pattern and I did this in large part to contrast with the often used page object pattern. In many automation libraries or frameworks, the idea of page objects is to encapsulate logic related to the page, or a part of the page. The logic is the actions that someone can take on the page. But it’s also easy to allow page objects to become a form of “element store.” The problem is you introduce a chain of dependencies focused on these page objects. They tend to become harder to maintain as a test suite grows, which means automation engineers tend to have pages extending other pages, which just furthers the anti-pattern even more.
What you’ve seen with the screenplay pattern is a focus on composition over inheritance. And when you get into composition, you start to think more in terms of the workflow of behavior rather than a simple navigation of pages. This means that page objects in a screenplay pattern tend to be quite a bit more focused, sometimes just containing targets to represent elements, as you’ve seen in these posts.
You’ve seen that same idea of reducing the need for things to change by how focused tasks, actions, and questions are as well. The idea is that each component should only have one reason to change.
Finally, this overall design goal allows you to abstract over certain things, like, for example, the API that executes a browser (WebDriver) and the API that interacts with the browser (Selenium). When you have abstracted over those kinds of entities, you are not as bound to changes in their implementation. Further, you can smooth over some of their “rough spots” — i.e., unclear or heavily-implementation specific code of the API. This also allows you to extend the behavior of these entities in a way that still allows you to use their API, but with your own declarative code guiding your expression.
Overall, I hope this series of posts was helpful. I do think Serenity is one of the better libraries out there in terms of attempting to apply good patterns. I would encourage all automation engineers to look at the library. Even if your language of choice is not Java, focus instead on the screenplay pattern itself and think about whether it makes conceptual and practical sense. I would most definitely like to see this pattern implemented in other languages and see more testers talking about this pattern and how it can be reflective of the business domain.
Really great articles! Excellent work, and I really appreciate the effort.
Really helps me get started with Serenity.
We have tried to use selenium with Cucumber before it seemed like a lot of work,
maybe Serenity will be something for us.
Thanks for the comment, Rolf.
Indeed, Serenity is something I’ve been playing around with more and more. I do feel it has a lot of good aspects to it.
Serenity also hooks into Cucumber, incidentally. If it helps, I did provide a few recent posts on that specifically. Not only do I cover Cucumber working with Serenity, but I do digress a bit into whether Cucumber makes sense given a focus that Serenity has of generating living documentation.
This is an area I’m hoping to explore a little more as time goes on.
Hi Jeff,
the sample tess ran fine but after updating IntelliJ to 2016.2 I use to get this Exeption
I checked the location of chromedriver and tried with command line arguments as well as configuration files but nothing helped by now. Would you mind giving me a hint what might be wrong over here?
Kind Regards,
Martin
I also upgraded to IntelliJ 2016.2 and I don’t see the issue that you are referring to, unfortunately.
The “no implementation was bound” type error tends to be something with Guice and the injection mechanism. Serenity is certainly using that.
So what’s happening here appears to be in the internals for some reason. Just to be clear, you get this error when you run the following from the command line:
Is that correct?
Is it also correct to assume that you are not seeing any errors in the code itself? Meaning, is IntelliJ showing you any problems in any of the files. I wouldn’t think it is in this case because the problem, based on your error message, is how the WebdriverManager is being injected. But I just want to see if I can isolate what the problem is.
Hi Jeff,
Thank you for your really good material, can you please provide a small example how work with implicit/explicit waits using this pattern?
I apologize for the incredibly long delay on responding to this. I’m currently working on another iteration of tutorials for Serenity that do in fact cover various ways of waiting. Serenity is undergoing a series of changes right now so I’m trying to make sure the dust has settled enough to make a next set of tutorials useful.
Thanks for this series of posts. I am preparing a presentation of the Screenplay Pattern (which I’ll give in 2 days actually!), and your series of posts will for sure help me out a lot to convey the message.
Cheers from Argentina!
Hi Jeff, there seem to have been some changes in Serenity that make it difficult to follow along with this tutorial, for example I couldn’t find ‘Task’ in serenity-core 1.1.42, only Action.
Also, I’m trying to make this work with Cucumber, could you recommend any good, up-to-date tutorials combining Serenity with Cucumber?
Cheers
Unfortunately, Serenity’s development is (to me) a bit haphazard with a lot of little changes and some major changes introduced but with little fanfare and even less documentation. Interestingly, on the part you mention, consider these links:
https://github.com/serenity-bdd/serenity-core/blob/master/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/Action.java
https://github.com/serenity-bdd/serenity-core/blob/master/serenity-screenplay/src/main/java/net/serenitybdd/screenplay/Task.java
Note that Action is listed as deprecated, not Task. Yet, Action says to use Interaction. There are a lot of things that crop up like this in Serenity. There are internal aspects that are available but not documented — particularly when it comes to Cucumber integration. I’ve tried to participate in some development but the levels of indirection in the Serenity code base are a bit thorny to navigate.
As such, I haven’t really done much to update these tutorials mainly because it’s hard to keep up and even harder to see what’s intentional behavior in many cases. I’ve come to the personal conclusion that while I like what Serenity is doing, it’s perhaps a bit too heavy of a set of abstractions which is further dulling my interest to revisit it with a new series of posts. (Yes, I realize this makes me about as ambitious as a slug!)
That being said, let me do a bit of investigation about specific changes that maybe I can update here. For example, I would be curious about that Task / Action thing. Tasks are supposed to be a core part of the concept base. For example, see the bit in the official documentation about tasks or how tasks can build upon tasks. And tasks are still a core part of the pattern. See the diagram at that last link.