Screenplay Pattern with Java, Part 2

This post is following on the from the first post in this series. Here I’m going to dig a little further into the screenplay pattern, finishing up the execution of the test started in the first post. I’m focusing on the screenplay pattern itself here. That pattern can be applied in any programming language. I happen to be using Java, and I’m using a tool (Serenity) that already encapsulates this pattern.

I know it may seem obvious but I’ll just stress this point once: you really do have to go through the first post to continue with this one. I’m going to be building off of the code from that post. While I could just provide a repository and have you start from there, one of my goals in these posts is to get you into the cadence of writing screenplay-based tests. That cadence requires that you get a feel for what it’s like to construct these various elements from scratch, rather than relying on work someone already did.

Assuming you followed that post largely to the letter, then you’ll have a test file called AddNewTodo. It’s contents should look like this, minus the imports:

In this post I’ll continue fleshing out the test method. Specifically, it would be nice to make sure to actually add an item to the todo list and, most importantly, then verify that the list was appropriately updated with the item that was added. I’ll handle the verification part in the third part of this series. Here I’ll focus on actually performing the action that the test is meant to perform.

As I mentioned in the first post, I’m doing nothing very unique here. I’m following the context of tutorials that are already out there, literally showcasing these same examples. What I hope I’m doing a slightly better job of, however, is putting some surrounding context around those examples.

Adding the Test Action

Now, let’s add a step to our test that actually does the specific task that we want:

There’s a couple things to note here. One is that I’m now using a when() method, provided by the GivenWhenThen class to complement the givenThat() method already used. As I mentioned in the first post, these given/when/then methods are optional. So line 31 from above could have been written like this:

Another thing to note is that we have a task called AddATodoItem. As I talked about in the last post, you generally want to get into the pattern of writing screenplays-based scenarios as you want them to read. So, as of right now, there is no “AddATodoItem” class, which means I’ll need to create one. Eventually, as you build up your own domain-specific language, you’ll find yourself creating less and less new classes like this but the point is that the freedom is there. That freedom is something to be harnessed rather than feared.

Adding the new Task

Assuming we’re harnessing rather than fearing, let’s create a class called AddATodoItem. This, like the StartWith task from the previous post, will be created in the tasks package. Keep in mind that this package is in the src/main/java package root. This AddATodoItem class, as with StartWith, should implement the Task interface. And because our Task class is implementing the Task interface, you must provide the performAs() method. So your AddATodoItem class should start out its life like this:

One thing to keep in mind here is this idea of creating tasks. Notice how the basis for creating StartWith and AddATodoItem are exactly the same. They provide a consistent interface and thus a consistent entry point, which is the performAs() method. Recognizing this is part of getting into the cadence of how the screenplay pattern works. Or, to be slightly more precise, how Serenity works as it implements the screenplay pattern.

Make sure to add the import for this new class in your AddNewTodo test class:

Now we have to handle the called() method. If you remember from the last post, for the StartWith task we provided an anEmptyTodoList() method. This was a builder method but there wasn’t a lot for it to do. I did, however, mention that builder classes can be used to provide the configuration or setup for the task. One of those ways might be by providing variables. So now consider that in relation to that called() method, which will also be a builder method and most clearly takes an argument. In this case that argument is nothing more than a string that indicates the text to be used for the todo list item we are adding.

So let’s add that called() method to the AddATodoItem class. Your IDE will make this quite a bit easier, but essentially what you’ll end up with by default is this:

As I described in the last post, these methods will default to returning a Performable object but, in reality, you want this to either be an Action or a Task object, both of which implement the Performable interface. As we did with the StartWith task’s anEmptyTodoList() method, we’ll change this method to return an instance of the current class. I didn’t dig much into why you return on instance in the previous post. I will do so momentarily. I’ll also change the parameter ‘s’ to be something a little more readable:

We clearly don’t want to return null here. But what do we want to do? Well, we’re passing in that string which is going to hold the text of the todo item we want to add to the list. So what I want to do is return that. But I can’t return that because that’s a string and we just said that this method returns an AddATodoItem reference. This is where we go back to that instrumentation idea that I talked about in the previous post. Change the return statement in the called() method to this (and make sure you provide the static import):

I covered the idea of instrumentation a very little bit in the previous post. In the context of this post, let’s dig a little bit deeper. The instrumented() method takes two parameters. The first argument should be your “stepClass”. The second is any reference of type Object that will serve as parameters to the stepClass. This is an area where I think Serenity gets a little over complicated but I also think it’s largely a result of all this being done in Java, a static language.

Consider this bit of context: AddATodoItem.called() returns an instance of a Task. That instance is an instance of AddATodoItem. That instance is then evaluated later in the attemptsTo() method, which will be provided in the performAs() method. If you look back at the StartWith class, you’ll see we did exactly this in the previous post.

But why is all this necessary? This is largely, but not entirely, necessary to support the reporting and living documentation aspects. Keep in mind that our tests are reading somewhat like a description of the business rules as they are being carried out. Consider again:

That reads really nice. But in order to read that nicely, I have to make sure that called() is a static method so that I can call it directly on the class. But that class implements an interface (Task) and that interface provides a method (performAs) and it’s that method that I need called. But I’m not calling that method from my test. Instead, what I’m doing is using my static method — the called() method — to return an instrumented instance of the Task, along with a parameter that was passed to the static method.

I hope I worded that well enough. I would encourage you to read that again and consider that much of what you see in Serenity that may seem “complicated” is because the code is acting in service to the idea of allowing business-readable code that is in turn pushed up into a reporting layer. Further, that reporting layer does its best to make sure that the code is reconstructed into some form of natural language. This is really a key feature of Serenity because when the descriptive aspect in the code carries forward into the reporting, it allows various audiences, of varying technical levels, to understand what tests are actually demonstrating.

Now let’s put in the individual actions that will make up how this task is executed. These will go in the attemptsTo() to method, which is called on the actor instance. This is all done in the performsAs() method. Let’s be deliberate here and start with this:

As before, the performsAs() method encapsulates the actor instance method attemptsTo().

Action Classes for the UI

If you check back on the StartWith task, you’ll see that we used the Open class. That essentially got the browser going and got us to the right page. For the action part of the current test step, we’re already going to be at the right page. So now we need to perform some other UI actions. Again, I’m going to be very deliberate here and take this slow. Fill out the attemptsTo() method as such:

Here I’m using the Serenity UI interaction class Enter to enter a particular value. In this case the value is held by the variable item. Ah! So now you see how the instrumented call from the called() method is passing the string parameter item back to the performAs() method. Except … it’s not. Not really. If you’re using an IDE, you’ll notice that your item variable is indicating an error and the error is that the symbol cannot be resolved.

If you are a Java coder or used to static typed languages, you can probably guess what the issue is. I need a field variable that will store the name of the todo string that my test is going to add. So let’s provide that:

That’s great, but now you’ll have an error on the Enter class line and it will be indicating that the attemptsTo() method cannot be resolved. The problem here is related to instrumentation, but this time it’s hidden a bit behind the scenes. Consider that theValue() is a static method. In the Serenity code base, this method is returning an instance of the Enter class. But there’s no actual context provided. Specifically, you have to say what to enter the text held by item into.

Add the following to the attemptsTo() method:

The error shown in your IDE will go away but now we need to specify some parameter to the into() method. One of the things we can specify here is a locator. By locator I mean literally that: a locator string that uniquely identifies some web element found on the page. This is exactly what you would provide to a Selenium findElement() method. So let’s change that code to look like this:

Here the text field for entering a todo item on the TodoMVC application has an id of “new-todo”. Since we’re using WebDriver to manage the actor’s interaction with the application, the when the .into() method is ultimately executed, the result is Selenium WebDriver is used to perform it’s normal “find element” action and then a sendKeys() call is done to enter the text into the element, assuming it was found with the locator provided.

Now I’m going to show you something that can be frustrating when you are learning Serenity. Let’s try to run our test:

gradle clean test aggregate

You’ll find the execution works as it did when we finished the last post. But you’ll also find that the text we specified is not entered in the field. Yet everything passes in terms of test execution. There are no compile errors in our code. So what’s going on?

This is something that can trip you up when you get into layers of indirection in Java. In the AddATodoItem class remember that we provided the member field item But we never had that item value assigned when AddATodoItem was instantiated as part of that whole dance between our static called() method and the performAs() method.

In Java you handle this via a constructor. So you have to add a simple constructor to make sure that the item variable is assigned appropriately. Here’s what your entire class should look like once this is added:

With this bit of knowledge in mind, let’s revisit the functionality a bit again.

The item field, declared on line 10, stores the name of the todo item we want to add. We initialize the member variable in the constructor, provided on line 20. We then create an instrumented instance of the AddATodoItem class and pass the item argument to the AddATodoItem constructor, in line 25. So that’s a critical thing to understand about the instrumentation! It’s instantiating the class, which means a constructor other than the default needs to be provided if you are passing in information. This wasn’t needed with the StartWith task — or, rather, there we relied on the default constructor — because that task had nothing being passed into it. This is also why I have consistently changed the return type of these builder methods from the default Performable to the specific Task class.

This is one of those areas that I feel can be very confusing when you are coming to Serenity. I hope I’m not adding to the confusion with my explanations. Also keep in mind one of the core benefits of this approach from a code readability standpoint. The Task class name and the builder method combine to read like an English statement (i.e., AddATodoTask.called, StartWith.anEmptyTodoList).

Instrumented class vs Instrumented Method

I do want to show you one variation on coding the instrumentation line. This variation, which you will see in many tutorials, is a bit more wordy but does make it more clear what is happening. You can replace the return statement from the called() method with this:

Serenity provides this Instrumented class to provide a streamlined approach to creating task or action objects using a factory or builder pattern. Keep in mind that with the former approach you are importing the static method instrumented() from the Tasks interface. With the latter approach you are importing the Instrumented() class from the steps package. But the end result is the same. Feel free to use which version sits better with you.

Execute the Test

Now with the constructor in place, try to run the test again. You should see that it works. Actually works this time, as in the text we specified is entered in the text field. Check out the generated report (found in target/site/serenity/index.html). This will look exactly like what it looked like in the previous post but we have an additional line added to our steps which reads like this:

"Jeff enters 'Digitize JLA vol 1 collection' into #new-todo"

Serenity is doing the best it can to render the task and actions we provided into English. It has very little to go on except the locator we provided, so Serenity simply spits that back out. If you look back at StartWith, you’ll see we added a @Step annotation. Let’s do that for this task as well:

As before, the {0} refers to the first parameter that is passed in to the performsAs() method, which is the actor. You can refer to member variables on the class by using the # sign in front of the name of the member variable. If you were to run the test again and check the report, you would now see that the step line is rendered as:

"Jeff adds an item called 'Digitize JLA vol 1 collection'"

Much better!

Back to Actions

To get the job done, a high-level business task will usually need to call either lower level business tasks or actions that interact more directly with the application. In practice, this means that the performAs() method of a task typically executes other, lower level tasks or interacts with the application in some other way. For example, adding a todo item requires two UI actions: entering the text (which we just took care of) and hitting the return/enter key to make sure the list item actually gets added. Let’s handle that hitting return part.

Add the following logic to the action:

Here I’m using the Keys enumeration from Selenium and providing the RETURN constant. Notice that I imported this statically so the method call reads a little nicer.

One thing I want to make clear here is that Action classes — like Enter or Open — are, from a programmatic standpoint, very similar to Task classes. The difference is that Action classes deal with the actual interaction with an application. Both levels of detail — at the task and action level — are providing a convenient and readable DSL. In the case of the Action classes, these are letting describe common low-level UI interactions needed to perform a task. But they are also letting you abstract away a lot of the details of how a particular driver — in this case, Selenium WebDriver — actually handles those interactions.

If you run this, you’ll find that it works just fine: the item list text is entered, return is pressed, and the item is added to the list.

One thing that I don’t really like about the current attemptsTo() method for our AddATodoItem class is that hard-coding of the locator (“#new-todo”). Everything else reads fairly well, except that. Further, if that locator changes I would have to remember any tasks that utilize that text field and make sure to make the appropriate page.

This probably seems like the likely place for a page object, right?

Adding a Page Object

What we’ll do here is refactor the selector into a page object. Change the into() method parameter as such:

In what should be a common theme by now, we’re writing as we want it to read and worrying about implementation after. In this case, we have to decided where a TodoList class is going to go. Then we’ll create a constant on that class called “TODO_FIELD”.

We’re going to want to create a TodoList class and we can put this in the ui package. Remember that this package is in the root of src/main/java. We’ll have this class extend PageObject so TodoList should look like this initially:

Then let’s add a static constant for TODO_FIELD as such:

Execute the test again and you should find all works as before.

Targets for Actions

Let’s refine our page object slightly.

Serenity does provide the concept of a Target class. In this case, “target” refers to the “target of the action being taken.” From a Selenium WebDriver perspective this means, the target will be used to identify elements. The Target object associates a WebDriver selector (like our “#new-todo”) with a human-readable label. This label not only lets the code read a bit more declaratively, but it also keeps implementation details lower in the stack and provides the human-readable label as part of the test reporting.

But what actually is a “target” in the programmatic sense? Targets are somewhat like page object classes except that a target is responsible for one thing and one thing only, following the SRP principle. That “one thing” is encoding the knowledge of how to locate the elements for a particular UI component.

So let’s put this knowledge to use and see if it’s worth it. Change the TODO_FIELD line in your page object as such:

With these changes in place, go ahead and run the test again and generate the report:

gradle clean test aggregate

If you now check the report, you’ll see that the test step listed for the task is, as before

Jeff adds an item called 'Digitize JLA vol 1 collection'

However, underneath that you’ll see the action now reads quite a bit nicer:

Jeff enters 'Digitize JLA vol 1 collection' into 'What needs to be done?' field

As you can see that bit of text following “into” is pulled directly from the the() method that we called on the Target class.

This is an important point! A lot of coders don’t like to inflate code if that’s not necessary. You could argue that we’ve done just that here. I went from having one simple method call (into("#new-todo")) to having a class (TodoList) that in turn relies on Target class that calls a method (the()). So, yes, we do have more code. But this is code in service to a particular goal. Or, rather, two particular goals.

With all that being said, for now just keep in mind that the Target class is being used as a mechanism to associate an expressive bit of text (“what needs to be done”) with a WebDriver location strategy. That association is being kept out of the test and the task, and also being abstracted into the action. Speaking purely from a development perspective, I do believe this is an appropriate level of coupling while maintaining high cohesion.

In fact, speaking to the idea of intent-revealing and expressive code, someone could argue that “TODO_FIELD” doesn’t actually reflect the field that well, in terms of naming. In the application, the field has a placeholder text that says “What needs to be done?” Should we make our code reflect that? Let’s try it out. Change the .into() method parameter so it reads like this:

You’ll have to change the target constant accordingly in the TodoList class:

Necessary? Strictly speaking, not at all. Does it improve code readability. Perhaps. Another argument could be that tying the constant name for the field to its placeholder text is more brittle than using our previous generic “TODO_FIELD”. These are the micro-decisions that you’ll want to make as part of what you think makes the most sense.

As one more example of where you can keep “tidying” your code, you could add a static import of the TodoList so that your final AddATodoItem class look like this:

At this point these latest refactorings have done nothing to impact the output in the test report. They have been done solely to improve the readability of the code itself. That can be a worthwhile endeavor but it can also be a practice that’s taken too far. You have to define your own limits and tolerances for this.

The Design Goals

To close out this post, let me just briefly revisit some of the design goals.

The primary goal is making sure that the automation can reflect the business intent and the implementation back up to the reporting in a way that is readable by various people.

Another goal, however, is adhering to effective design practices, such as separation of concerns, putting the more volatile elements (like locators) as far from the more unchanging elements (like test intent) as we can.

We’ve seen the use of abstraction layers as a guiding focus through both of these posts. There was a clear separation of actors and their respective drivers (in our case, a browser). This makes it easy to have more than one actor when that makes sense but it also makes it easier to swap out drivers for the same actor.

There is a clear distinction between Task classes and Action classes. This makes it easier for you to write layered tests more consistently and provides you a heuristic for deciding where to put higher-level abstractions (in the Tasks) and lower-level implementation details (in the Actions).

The Action classes allow you to abstract yourself away from Selenium WebDriver. The actions we’ve taken that utilize the WebDriver components are higher-level than the API calls made directly to Selenium. In the third post in this series, you’ll see that the same thing applies to Question classes. The separation of Ability and Action classes, along with the idea of Target classes, means that Selenium is kept very much behind the scenes. This allows you to leverage the full power of Selenium but to also extend it when need be.

In many ways, Serenity is acting within the principle of convention-over-configuration. Yet that principle is only enforced to a certain extent, such as when certain classes implement an interface that requires specific methods. One of the primary goals of a convention-based approach is consistency. Such consistency aids discoverability. Do keep in mind, however, that none of the packaging structures I’ve shown you are enforced by the tool. A convention-based approach aims for structural simplicity in that there is a place for everything. But it’s up to you and your team to decide what those places are.

Finally, there’s the notion of readability. When a convention-based approach provides a DSL, it’s possible to make sure that code is by definition intuitively discoverable. Serenity applies a lot of this itself, as you’ve seen. But there’s also a key part of this overall DSL that you are designing as you create your scenarios. So there is a level of discipline required in terms of how to write expressive statements of test that tell you what is being done from the user’s perspective.

Share

About Jeff Nyman

Anything I put here is an approximation of the truth. You're getting a particular view of myself ... and it's the view I'm choosing to present to you. If you've never met me before in person, please realize I'm not the same in person as I am in writing. That's because I can only put part of myself down into words. If you have met me before in person then I'd ask you to consider that the view you've formed that way and the view you come to by reading what I say here may, in fact, both be true. I'd advise that you not automatically discard either viewpoint when they conflict or accept either as truth when they agree.
This entry was posted in Automation, Java, Serenity. Bookmark the permalink.

Leave a Reply

Your email address will not be published. Required fields are marked *