Screenplay Pattern with Java, Part 1

In this tutorial post, I’ll begin covering how to apply the screenplay pattern with the Serenity framework. This is a “roll-up your sleeves and code” post. Here I will take a measured approach to demonstrating the screenplay pattern in the context of a working example.

I should note right at the start that most of this material already exists out there. John Ferguson Smart, who wrote Serenity, has posted a lot of material on the tool. Without intending offense, I will say that I think most of this material is very defocused and hard to utilize in a way that gets people thinking about whether the screenplay pattern makes sense for them or how they can best put it to use.

By necessity, this post will assume that you are comfortable enough working with Java and Gradle. I use Gradle instead of Maven, but if you prefer Maven it’s plenty easy to change my Groovy-based build file into an XML-based one for Maven. I don’t assume any particular IDE and, again, that goes along with you being comfortable enough in Java to use tools that support it.

If you are curious about some of the theory behind screenplay, you might want to check out my post Screenplays and Journeys, Not Page Objects. I also did a slightly easy introduction to using Serenity in my post Java Automation with Serenity. That latter post covered nothing about the screenplay pattern however.

Okay, enough context. Let’s get to work!

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 init

That 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:

You’ll notice here that I have some Serenity libraries in the compile scope instead of just the testCompile scope. This is on purpose and will be explained later.

Initially, I’ll be using Serenity with JUnit as the test runner. You can also use Cucumber-JVM or JBehave as the test runner, if such is your preference. I mention this simply because I’ve included all of those dependencies in my build file but you only have to include the runner(s) you plan to actually use. My personal view is that when learning the framework as well as the screenplay pattern, I would keep the tooling as minimal as possible until you have the basics understood. Then you can decide whether you want to add more abstraction layers.

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 with src/test/java and src/main/java as roots. In the src/test/java package, create a package called:

com.testerstories.tutorial.todos.features.add_todo

You 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:

Serenity provides a SerenityRunner that hooks into JUnit. So we use the standard RunWith class from JUnit and tell it to use that SerenityRunner. Pretty standard stuff if you’ve used test runners in Java before.

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.

As you can see, creating an actor means you create an instance of the Actor class and then call the named() method to provide friendly name for the actor. This friendly name is what will be used in test reports.

You might wonder what the point is of having such a friendly name. Consider that you can create multiple actors and those actors might be named something like “Admin User”, “Read-Only User”, “Limited User” and so on. In this case, those actors could be applied to specific test scenarios but, because they are different actors, different conditions can be operative in the scenario based on the actor.

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.

Here I’m saying the managed driver should use the Chrome browser. If you didn’t have this in place, Firefox would be used by default, as per current Selenium WebDriver standards.

Please note that if you go with any other driver than the default, you must have that driver available on the path of your relevant operating system. Or you must specify a path. One easy way to do this is to use a serenity.properties file, which you can place in the root of you project. Within that file, put lines such as this:

Make sure to replace path_to_chromedriver with your actual path. Again, though, you might find it easier to simply make ChromeDriver available on your path. For example, on MacOS, you can do 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.

What this is basically saying is that Jeff has the ability to browse the web. In fact, it almost reads exactly like that. This is a key thing that you should notice not just about the screenplay pattern, but about the rising trend of automation to utilize a DSL that is indicative of what is being done.

Each of the actor’s abilities is represented by an Ability class. In this case, that ability class is BrowseTheWeb. These ability classes are used to help deal with the things an actor would need to do order to perform the ability. In this case, this means the actor instance (jeff) doesn’t have to know how to utilize WebDriver in order to drive the browser. Instead, the 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:

As with BDD style solutions, you can use “given / when / then” to make the intent of a test more obvious. The givenThat(), when() and then() methods are static methods imported from the GivenWhenThen class. As you’ve seen with this simple example, these methods are entirely optional. It’s up to you to decide if the extra bit of syntax makes things better or more cumbersome. For now I’ll keep the statement as I refined it above.

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:

You can see from the scenario title that Jeff’s goal is to add the first todo item to a list. You’ll also note that this is following a convention borrowed more from languages like Ruby rather than Java in that underscores are used to make readability clearer. Quite honestly, this is always a matter of debate in terms of what is and is not “more readable.” Rather than rehash that debate, consider “should_be_able_to_add_the_first_todo_item” and “shouldBeAbleToAddTheFirstTodoItem”. From a cognitive science standpoint, it’s beyond dispute that the human eye and brain definitely tends to have less cognitive friction with the underscore approach. Regardless of that, decide for yourself and proceed accordingly.

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.

Depending on your background, you might note that this breakdown is very similar — in fact, identical — to how much of User Experience (UX) design is conceptualized. The goal is the ‘why’, the tasks are the ‘what’, and the actions are the ‘how’.

One of the core conceptual aspects of this approach is that the goal exists to provide a clear distinction between tasks and actions. When this conceptual model is supported by an automation framework, the theory is that this makes it easier for teams to write layered tests with more consistency and, perhaps most importantly, in a way that facilitates discussion with business teams as well as developers.

The goal, in our case, is the test or scenario name (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.

Here the “StartWith” class and the “anEmptyTodoList” method are not defined. That’s important to understand. I’m typing this up as I want it to read, worrying about creating the implementation later. This is one of the hardest parts I’ve found automation engineers have to deal with, unless they are used to coding this way. From a conceptual standpoint, do keep in mind that “StartWith.anEmptyTodoList” is a task.

As before, you could leave off the givenThat() part and have this:

The argument here would be that the intent of a pre-existing state was implicit in the wasAbleTo() method. Again, it’s up to you. 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.

Okay, so I have task that I want to carry out and I need to create the implementation of the task. So where do I create these things? You can use any of the Java packaging structure that you want. I’m going to put this implementation type stuff in src/main/java. This is why some of my Serenity libraries were indicated as being in the compile scope rather than just the testCompile scope.

In the src/main/java package, create a domain package added to the business ability package just like you did for src/test/java. Breaking this down, first create the package com.testerstories.tutorial.todos. (Or use whatever domain part you put in place.) Then create another package within todos called tasks. So your full package for now is:

com.testerstories.tutorial.todos.tasks

Under 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:

You’ll notice this class doesn’t compile. The Task interface requires that you implement a single method called performAs(). So go ahead and add that. Your IDE will likely warn you of this and let you apply the method automatically. This what the class should look like:

One thing to note immediate is that I changed the “t” parameter, in the performAs() call, to “actor”.

I realize that right now it can feel like you are getting way off in the weeds. So let’s focus a bit on what we’re doing here.

One of the core conceptual concepts of the screenplay pattern is that an actor performs a sequence of tasks. In Serenity, this mechanism is implemented via the use of the Command pattern. Specifically, the execution of a task is handled by the actor instance (jeff) executing a task (StartWith) by invoking a special method called performAs() on the corresponding Task object. The actor instance, as you can see, is passed into the performAs() method so that the context is maintained.

So, for right now, just keep in mind that Tasks are just objects that implement the Task interface. Since this is an interface, any Task object needs to implement the performAs() method provided by that interface and this method takes in an instance of an actor. In fact, you can think of any Task class as basically a performAs() method with the addition of some other utility and/or helper methods.

I might sound like I’m beating a dead horse here, but I want to make sure the concepts are clear: an actor performs a task. They do so either by performing other, smaller tasks or by interacting with the application in some way.

In the case of our StartWith task, we need to open up the TodoMVC application. Let’s add a small bit of logic to the StartWith class:

Keep in mind that we got here by our line in the test that reads like this:

The wasAbleTo() method takes a Task object — or a list of such objects — and successively calls the performAs() method for each task.

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:

Here the actor is attempting some action via the attemptsTo() method. In this case, the action is that of interacting with the web application using the Open action class. Serenity comes with certain built-in UI related interaction classes. As their name would imply, these are used interact with browsers and web pages. Other classes in this context are Click, Enter, Hit, Select, Scroll, and so on. I’ll get a bit more into those in the second post in this series.

But what is applicationHomePage above? Well, the 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:

Considering just what we’ve done so far, there are three methods and one class there that we didn’t write at all. This is a DSL on top of an API that Serenity provides. As with any such framework, getting familiar with that DSL+API is crucial to being productive with the framework. I find this is a lot more challenging for automation engineers who have lived in Java or C# environments, not so much for those who cut their teeth in Python, Ruby, or JavaScript environments.

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:

Now we need to create that page object. But where should it go? Create another new package in src/main/java:

com.testerstories.tutorial.todos.ui

Notice 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:

This is going to be a very simple page object. In fact, it will have no logic at all. It will simply provide a @DefaultUrl annotation to indicate what URL should be used when the browser is opened on this page.

I should note something here that’s probably obvious and yet doesn’t get called out much. The @DefaultUrl annotation indicates just that: the default page that will be viewed when this test is run. This value, however, can be overridden. It is possible to specify a particular property (webdriver.base.url) in a serenity.properties file that lets you set a specific URL that will be used for tests, overriding any default URLs if they are present. I mention that just for those automation engineers who worry about hardcoding something as important as the entry point URL in a test.

Continuing on, make sure your StartWith class has the appropriate import in place:

It’s surprisingly easy to forget that sometimes!

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:

Here we’ll change that so it returns an instance of the current class:

If you are wondering about Performable, keep in mind that an Actor class is responsible for performing either a task or an action. Specifically, a Task or Action interface. And these interfaces are both extended from the Performable interface.

But what should we do in this method? If you know Java, you know that returning null is likely not going to be a good thing here. And you would be right. What we really want to do is just return this class since we have no specific action to take. However, you can’t do something like this:

Yet that is exactly what you want to do! Serenity provides an instrumented() method to make this possible. This instrumented() method is provided as part of a Tasks class, the details of which would derail us a bit at this point. For now, change your logic to look like this:

This might seem like a bit of magic. Instrumentation is kind of a complicated subject in many ways. The idea came out of a technique known as “byte code instrumentation.” I’m by no means an expert in this so I shouldn’t really be explaining it but essentially the idea is that when a class is loaded via the JVM classloader, you can actually alter the corresponding byte code to introduce specific logic. One relatively common use of instrumentation is the addition of byte code to methods for the purpose of gathering information that can be utilized by other tools, such as analysis tools or reporting tools.

So the instrumented() method provided by the Serenity framework enables reporting based on information from the task, such as writing each task into a test report along with any actions that were performed as part of the task. Here the StartWith.class is instrumented to provide information about what was done.

More importantly this action is what is essentially instantiating the StartWith class and executing the performsAs() method. Since we don’t provide a constructor, the default constructor will be used.

Beyond this implementation I just showed you, there is an Instrumented class provided in Serenity. And I mention this not to focus on it right now but only because you may see it referenced in various tutorials. This class is actually different from the instrumented() method on the Tasks class but the overall function is largely the same. The Instrumented class is used to create task or action objects using the builder pattern. My current example is way too simple to require this but I’ll revisit this in the second post in this series.

Execute (Finally!)

Okay, moment of truth here. Try this:

gradle clean test aggregate

If 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:

The actor is “Jeff” (because that’s what I named him). So Jeff attempted to Open a browserOn() the() applicationHomePage. Serenity took all that and said “Jeff opens the Application home page.” This is how Serenity is trying to adhere to the living documentation approach by essentially pushing from the code up to the English level of description.

Yet something is off here, right? That line in the test report is talking about the action but the task itself is sort of getting lost in the details. And that’s because the opening of the browser and navigation to the application page is not really what I want to convey. What I want to convey is that Jeff started off with an empty todo list.

So let’s make the text in the report reflect that. Go back to the StartWith class and add this:

The @Step annotation tells Serenity how this step should be written in any test reports. The {0} expression represents the Actor variable that is passed into the performAs() method.

With that change in place, run your gradle command again. Now you’ll see an appropriately named step of “Jeff starts with an empty todo list.” That’s my task. Do note that underneath that step you’ll see the previous “Jeff opens the application home page”, which is the action taken in service of the task.

This is really important! What this lets you do is essentially contextualize your test execution with details but without having to encode all of this in the readable DSL that we’ve been building up. This can give you a supreme amount of flexibility in terms of how you express the execution of your tests, particularly when you focus on tests that demonstrate business behavior.

This is a good place to stop our coding. You might notice that I didn’t really check if there was an empty list. I just assumed that there was because that’s the state of opening up the TodoMVC application. Don’t worry too much about that for right now. In the next post I’m going to cover more test logic.

Before closing out this post, let’s ask a key question that should be asked of any particular way of handling automation.

Is This Approach Worth It?

Even with my modest example as presented here, you can probably see how Serenity follows a convention-based approach to the construction of test logic and that logic is stated in terms of a DSL wrapped around an API. On the positive side, this can be a very powerful approach. On the negative side, it can be a lot to take in for people, particularly if you are not used to convention-based frameworks or approaches that focus heavily on a provided DSL.

Beyond that, I would argue there are a lot of good things here that an automation framework should adhere to: separation of concerns, a focus on SOLID design principles, high cohesion, and low coupling. Further, test organization is also important and the nature of separate classes suggests certain package structures, which I’ve only briefly covered here.

As you’ve barely seen, Serenity relies quite a bit on instrumenting code, whether that be via annotations or actual instrumentation methods. I say “barely seen” because I’ve only really scratched the surface of this functionality. I bring this up because some automation engineers feel that instrumented code has no place in a test framework and can, in fact, make things more complicated than they need to be. There is definitely more than a little truth to that. Usually the “more complicated” part depends entirely on how much of the instrumentation surfaces to the higher level DSL. Dynamic languages, like Ruby and Groovy, are generally much better at keep instrumentation hidden.

Is This Exactly Like BDD, Except Different?

Consider what you’ve seen here in relation to BDD tools like SpecFlow, Behat, Cucumber, JBehave, Lettuce, Gauge, and so on. In those tools, you are dealing more with an executable specification approach. The overriding theory guiding those tools is that it must be possible to quickly write such specifications and convert them into an format that can executed as code. In those cases, you write a natural language level of abstraction, often with a structuring element like Gherkin (Given/When/Then). This is no more or less a DSL than what we looked at here but, clearly, the level of abstraction is different. As I mentioned earlier, Serenity does allow you to use Cucumber-JVM and JBehave as test runners and I hope to explore that in a future post.

So what does this say for Serenity and its approach? Well, obviously that’s going to be up to you as a practitioner. I will say this: as with any such approach, a fairly good amount of discipline is initially required to design a readable, DSL-on-API that is made up of well-organized tasks and actions. The question is whether that work will accrue any benefits? I would argue that it can but those benefits take time to become apparent. Specifically, they will become most apparent when the test suite has to scale. In those cases, your libraries of reusable components should allow you to maintain, and even accelerate, the test writing process to a level that is sustainable enough to be responsive to changing business needs and development conditions.

Liz Keogh made an interesting point in her article At 70 words per minute, no one can hear you scream, when she said “efficient programming is all about reducing the gaps between those flashes of inspiration when code flows naturally, makes sense, feels clean and maintainable and not awkward to manage.” I believe that is exactly the flow you are going for when you write and rely upon a test-focused DSL.

This was a long post. My hope is that the content makes Serenity more of an understandable tool, but also makes the context in which such a tool should operate more clear. As the post title suggests, there will be follow ups to dig more into the Serenity tool and the screenplay pattern.

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, Selenium, Serenity. Bookmark the permalink.

7 Responses to Screenplay Pattern with Java, Part 1

  1. Martin says:

    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

  2. Martin says:

    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:

    test {
    
        System.setProperty("webdriver.chrome.driver","D:\\lib\\chromedriver.exe")
    
        /* Pass all system properties: */
        systemProperties System.getProperties()
    }
    • Jeff Nyman says:

      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.

  3. Ronald Borman says:

    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.

    • Jeff Nyman says:

      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.

  4. Mike says:

    Great article! When to expect part 2?

Leave a Reply

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