Writing Automated Acceptance Tests Using Serenity and the Screenplay Pattern

rubicon_dev

RUBICON

Posted on May 27, 2021

Writing Automated Acceptance Tests Using Serenity and the Screenplay Pattern

Serenity is a powerful library for writing automated user acceptance tests. It uses test results to generate world-class test reports that document and describe what your application does and how it works.

Setting Up Serenity BDD

To better understand Serenity, let's write an example test for RUBICON's homepage.

For this tutorial, we are going to use JUnit. Serenity can also be used with BDD tools such as JBehave or Cucumber. The complete source code for this tutorial can be found here or you can tag along by downloading the Serenity jUnit startup project here.

For this tutorial, we'll be using the browser Firefox. To begin, let's import the project into IntelliJ.

Let's follow these steps:

  1. Select Import Project in IntelliJ, then find the downloaded project and select the folder with the build.gradle file and click Open.
  2. Select Import project from external model and select Gradle, click Next.
  3. On the next window select Only Use default Gradle wrapper and click Finish.

Now we will need to wait for Gradle to build the project. To confirm that everything works, we can find the TestStory file and run the TestExample test. The test should display Google's homepage.

Note: In the Screenplay pattern, tests are presented from the perspective of the user. A user that interacts with the web app is called an actor. Everything revolves around actors in the Screenplay pattern. Actors have special abilities such as being able to open web pages. They can also perform tasks and ask questions about the state of the web app.

Now let’s move onto the next steps.

The outline for the test we are going to write is:

Given that Nermin can see the Rubicon home page
When Nermin opens the Development link
Then Nermin should see the Development page

As you can see, we are using Gherkin to describe the test case. Using the given-when-then structure for test scenarios is helpful because it clearly separates what the pre-condition (given), the action performed (when) and the expected postcondition (then) is.

Now in our features package, let’s create a new test class called Navigation and add the following code:
package features;

import net.serenitybdd.junit.runners.SerenityRunner;
import org.junit.runner.RunWith;

@RunWith(SerenityRunner.class)
public class Navigation {
}
Enter fullscreen mode Exit fullscreen mode

Opening a web page

To open a web page we will need to use a web browser. We can do that by declaring a web driver field called webBrowser. The @Managed annotation will let Serenity manage our web driver.

@Managed
WebDriver webBrowser;
Enter fullscreen mode Exit fullscreen mode

Before we can do anything in Serenity, we will need an actor. We will name our actor Nermin and give Nermin the ability to use a web browser.

private Actor nermin;

@Before
public void userCanBrowseTheWeb() {
    this.nermin = Actor.named(“Nermin”);
    nermin.can(BrowseTheWeb.with(webBrowser));
}
Enter fullscreen mode Exit fullscreen mode

The @Before and @After annotations are used in JUnit to mark those methods as set up and tear down methods respectively.

To write our test, the first thing we are going to do is to open RUBICON’s homepage. Our actor Nermin will have to perform a task to open RUBICON's homepage.

Note: In the Screenplay pattern, tasks represent the high-level steps the user needs to perform in order to achieve a goal.

To write the OpenTheRubiconHomePage task, we will create a new package called tasks. Inside tasks, we will create another file called OpenTheRubiconHomePage.java. To make this class a Serenity task, it needs to implement the Task interface which has one method performAs.

public class OpenTheRubiconHomePage implements Task {
    @Override
    @Step("{0} opens the rubicon home page")        
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(Open.url("https://www.rubicon-world.com"));
    }
}
Enter fullscreen mode Exit fullscreen mode

To open a web page, we will use the Open action class that Serenity provides for us.

To make the actor call the Open task, we will need to use the attemptsTo method.

The string in the @Step annotation is used to modify the Serenity report. The {0} will be replaced by the name of the actor that calls this task.

Note: Actions are similar to tasks, but the difference is that actions perform low- level interactions on the web page such as clicking on an element or moving the mouse cursor.

Alt Text

This is what the task will look like in the Serenity report.

Note: The reports Serenity generates can be found in the target/site/serenity folder located in the project folder.

Now we are going to declare it in our Navigation class with the following line:

@Steps
OpenTheRubiconHomePage openTheRubiconHomePage;
Enter fullscreen mode Exit fullscreen mode

Adding the @Steps annotation will allow Serenity to initialize the openTheRubiconHomePage task. This will only work for simple tasks that take no parameters. Later on in the blog, we’ll take a look at how to create tasks that accept parameters.

Since we now have our task for opening the RUBICON web page, we will call it in our test:

@Test
public void shouldBeAbleToOpenTheDevelopmentService() {
    givenThat(nermin).wasAbleTo(openTheRubiconHomePage);
}
Enter fullscreen mode Exit fullscreen mode

It is actually possible to call this task without the givenThat method:

nermin.wasAbleTo(openTheRubiconHomePage);
Enter fullscreen mode Exit fullscreen mode

But if we use static methods like this, we can increase the readability of the code. Some other methods of the GivenWhenThen class are:

andThat()
when()
then()
and()
Enter fullscreen mode Exit fullscreen mode

Interacting with the Web Page

The next thing we need to do is to create a task that will both open the services dropdown and click on the Development service link.

To do that, we will need to find the web elements for the Service menu and links.

Now we can create a new package called targets and within it, a new class called RubiconHomePage where the following code can be added:

public class RubiconPage {
    public static Target SERVICES = Target.the("services dropdown")
        .locatedBy(".nav-services");
    public static Target SERVICE_ITEM = Target.the("service item")
        .locatedBy("//li[contains(@class,'dropdown-item')]//a[contains(text(),'{0}')]");
}
Enter fullscreen mode Exit fullscreen mode

The Target class is used to associate CSS selectors and XPaths with meaningful labels that will be used in the Serenity report. By separating the web elements in their own files, we will reduce duplication that can result from using hardcoded strings.

To open the Development service, we will first need to hover over the Services dropdown and then click on the Development service. This task will require the name of the service passed in as a parameter. This task will be created by calling the named method.

The complete code for our OpenServicePage will look like this:

public class OpenServicePage implements Task {

    private final String service;

    public OpenServicePage(String service) {
        this.service = service;
    }

    public static OpenServicePage named(String service) {
        return Instrumented.instanceOf(OpenServicePage.class).withProperties(service);
    }

    @Override
    @Step("{0} opens the service named #service") 
    public <T extends Actor> void performAs(T actor) {
        actor.attemptsTo(
            MoveMouse.to(SERVICES),
            Click.on(SERVICE_ITEM.of(service))
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

As you can see, we are calling two actions in the attemptsTo method, which can take one or more tasks. The first one is the MoveMouse action that will move the mouse cursor to the Services dropdown and the Click action will click on the service with the name passed in. Using the “of” method on the service item will pass into the target the parameter service, which will be replaced with {0} in the XPath string.

To type values into input fields, we can use the Enter action class:

actor.attemptsTo(Enter.theValue("Hello World").into(TARGET));
Enter fullscreen mode Exit fullscreen mode

To enter only a single key, we can use the Hit class:

actor.attemptsTo(Hit.the(Keys.ENTER).into(TARGET));
Enter fullscreen mode Exit fullscreen mode

To select values either by visible text, index or value from a dropdown we can use the SelectFromOptions action:

actor.attemptsTo(SelectFromOptions.byVisibleText("Hello World").from(DROPDOWN));
Enter fullscreen mode Exit fullscreen mode

Now that we have our new task we need to call it in our test:

@Test
public void shouldBeAbleToOpenTheDevelopmentService() {
    givenThat(nermin).wasAbleTo(openTheRubiconHomePage);

when(nermin).attemptsTo(OpenServicePage.named("Development"));
}
Enter fullscreen mode Exit fullscreen mode

Asking About the State of the Web Page

For the final part of this test, we will write our then part or the postcondition of the test.

In the Screenplay Pattern, actors can ask questions about the state of the web page. The purpose of the questions is to answer a precise question about the state of the web application, from an actor’s point of view.

To confirm that we are on the right web page, we are going to write a question that will return the heading of the current page.

This is what the question will look like:

public class CurrentPage implements Question<String> {
    public static Question<String> heading() {
        return new CurrentPage();
    }

    @Override
    public String answeredBy(Actor actor) {
        return Text.of(HEADING).viewedBy(actor).asString();
    }
}
Enter fullscreen mode Exit fullscreen mode

Questions need to implement the Question interface where T is the return type of the question. That interface has one method called answeredBy that needs to be implemented.

HEADING is just a new target we added to the RubiconHomePage class:

public static Target HEADING = Target.the("Heading").located(By.tagName("h1"));
Enter fullscreen mode Exit fullscreen mode

The Text class is used to read text values in a more fluent way. Another way to read the text would be to call the resolveFor method on the target to get the web element. This is a type of WebElementFacade and it has a method called getText() that will return the text on it.

public String answeredBy(Actor actor) {
    return HEADING.resolveFor(actor).getText();
}
Enter fullscreen mode Exit fullscreen mode

Assertions in Serenity Screenplay look like this:

then(nermin).should(seeThat(CurrentPage.heading(), equalTo("Software engineering.")));
Enter fullscreen mode Exit fullscreen mode

We can begin an assertion by calling the should method on the actor. Then we can pass a question into it and a hamcrest matcher.

Now that we have our question, we can finally complete our test:

@Test
public void shouldBeAbleToOpenTheDevelopmentService() {
    givenThat(nermin).wasAbleTo(openTheRubiconHomePage);

    when(nermin).attemptsTo(OpenServicePage.named("Development"));

    then(nermin).should(seeThat(CurrentPage.heading(), equalTo("Software engineering.")));
}
Enter fullscreen mode Exit fullscreen mode

Sometimes there will be situations in which we have to wait for a web page to load, in that case, we can retry the assertion by using the static method eventually:

then(nermin).should(eventually(seeThat(CurrentPage.heading(), 
        equalTo("Software engineering."))));
Enter fullscreen mode Exit fullscreen mode

After we run the test, Serenity will generate the following report for us:

Alt Text

Final Words

As you can see, tests in Serenity are written from the perspective of the user or actor. Tests coded in this way are far more readable and follow good engineering principles, such as the single responsibility principle. By splitting up our tests into tasks, questions, abilities, and actions we avoid duplication and create reusable parts that will speed up the development process of the test.

But that is not biggest advantage of using serenity over other frameworks. The strength of serenity lies in its great reports. Serenity is not just a library that generates test reports, the real goal is to produce living documentation for your product. That is a concept that comes from the world of Behavior Driven Development. Why living documentation, because it documents how the application works and what the business rules are in a way anyone can understand it, and it is living because it is generated by the automated test suite and it is always up to date. With that all said I encourage you to give Serenity BDD a try.

For more information about Serenity, you can check out the official Serenity website.


Original blog post: Writing Automated Acceptance Tests Using Serenity and the Screenplay Pattern

💖 💪 🙅 🚩
rubicon_dev
RUBICON

Posted on May 27, 2021

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related