The new Certification Class of REST With Spring is out:

>> CHECK OUT THE COURSE

1. Overview

In this article, we’ll have a quick look at the Screenplay Pattern in Serenity BDD. We suggest you read the basics of Serenity BDD first before reading this one. Also, the article on Serenity BDD integration with Spring might also be interesting.

Screenplay, introduced in Serenity BDD, aims to encourage good testing habits and well-designed test suites by enabling teams to write more robust and reliable tests. It is based on the Selenium WebDriver and the Page Objects model. If you’ve read our introduction to Selenium, you’ll find these concepts rather familiar.

2. Maven Dependency

First, let’s add the following dependencies to the pom.xml file:

<dependency>
    <groupId>net.serenity-bdd</groupId>
    <artifactId>serenity-junit</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>net.serenity-bdd</groupId>
    <artifactId>serenity-screenplay</artifactId>
    <version>1.4.0</version>
</dependency>
<dependency>
    <groupId>net.serenity-bdd</groupId>
    <artifactId>serenity-screenplay-webdriver</artifactId>
    <version>1.4.0</version>
</dependency>

The latest versions of serenity-screenplay and serenity-screenplay-webdriver can be fetched from the Maven Central Repository.

We also need web drivers to perform screenplay – either ChromeDriver or Mozilla-GeckoDriver will do. In this article, we’ll use the ChromeDriver.

The following plugin configuration is required to enable WebDriver, in which the value of webdriver.chrome.driver should be the relative path of ChromeDriver binary file in our maven project:

<plugin>
    <artifactId>maven-failsafe-plugin</artifactId>
    <version>2.20</version>
    <configuration>
        <systemProperties>
            <webdriver.chrome.driver>chromedriver</webdriver.chrome.driver>
        </systemProperties>
    </configuration>
</plugin>

3. WebDriver Support

We can make Serenity manage WebDriver instance by marking @Managed annotation on a WebDriver variable. Serenity will open an appropriate driver at the start of each test, and shut it down when the test is finished.

In the following example, we initiate a ChromeDriver and opens Google to search for ‘baeldung’. We expect Eugen’s name to be present in the search results:

@RunWith(SerenityRunner.class)
public class GoogleSearchLiveTest {

    @Managed(driver = "chrome") 
    private WebDriver browser;

    @Test
    public void whenGoogleBaeldungThenShouldSeeEugen() {
        browser.get("https://www.google.com/ncr");

        browser
          .findElement(By.name("q"))
          .sendKeys("baeldung", Keys.ENTER);

        new WebDriverWait(browser, 5)
          .until(visibilityOfElementLocated(By.cssSelector("._ksh")));

        assertThat(browser
          .findElement(By.cssSelector("._ksh"))
          .getText(), containsString("Eugen (Baeldung)"));
    }
}

If we don’t specify any parameters for @Managed, Serenity BDD will use Firefox in this case. The whole list of supported drivers by the @Managed annotation: firefox, chrome, iexplorer, htmlunit, phantomjs.

If we need to test in IExplorer or Edge, we can download web drivers from here(for IE) and here(for Edge) respectively. Safari WebDriver is only available on MacOS under /usr/bin/safaridriver.

4. Page Objects

Serenity Page Objects represent a WebDriver page object. The PageObject hides WebDriver details for reuse.

4.1. Refactor Example using PageObject

Let’s refine our previous test using PageObject first by extracting element locating, searching and result verifying actions:

@DefaultUrl("https://www.google.com/ncr")
public class GoogleSearchPageObject extends PageObject {

    @FindBy(name = "q") 
    private WebElement search;

    @FindBy(css = "._ksh") 
    private WebElement result;

    public void searchFor(String keyword) {
        search.sendKeys(keyword, Keys.ENTER);
    }

    public void resultMatches(String expected) {
        assertThat(result.getText(), containsString(expected));
    }
}

WebElement represents an HTML element. We can interact with web pages through APIs of the interface. In the example above, we used two ways of locating web elements in the page: by element name and by element’s CSS classes.

There are more approaches to applying when finding web elements, such as find by tag name, find by link text, etc. Refer to our guide to Selenium for more details.

We can also replace WebElement with WebElementFacade, which provides more fluent APIs to deal with web elements.

As Serenity will automatically instantiate any PageObject fields in the JUnit test, the previous test can be rewritten into a much cleaner one:

@RunWith(SerenityRunner.class)
public class GoogleSearchPageObjectLiveTest {

    @Managed(driver = "chrome") 
    private WebDriver browser;

    GoogleSearchPageObject googleSearch;

    @Test
    public void whenGoogleBaeldungThenShouldSeeEugen() {
        googleSearch.open();

        googleSearch.searchFor("baeldung");

        googleSearch.resultMatches("Eugen (Baeldung)");
    }
}

Now we can search using other keywords and match the related search result without making any changes to the GoogleSearchPageObject.

4.2. Async Support

Nowadays, many web pages are served or rendered dynamically. To deal with such cases, PageObject also supports many rich features that enable us to inspect statuses of elements. We can check if the elements are visible, or wait until they are visible before proceeding.

Let’s enhance the resultMatches method by ensuring that the element we want to see is visible:

public void resultMatches(String expected) {
    waitFor(result).waitUntilVisible();
    assertThat(result.getText(), containsString(expected));
}

If we don’t expect to wait for too long, we can explicitly specify the timeout on waiting actions:

public void resultMatches(String expected) {
    withTimeoutOf(5, SECONDS)
      .waitFor(result)
      .waitUntilVisible();
    assertThat(result.getText(), containsString(expected));
}

5. Screenplay Pattern

The Screenplay Pattern applies SOLID design principles to automated acceptance testing. A general understanding of Screenplay Pattern can be explained in the given_when_then context as:

  • given – an Actor that is capable of performing some Task
  • when – the Actor performs the Task
  • then –  the Actor should see the effect and verify the results

Now let’s fit our previous test scenario into Screenplay Pattern: given a user Kitty that can use Google, when she searches ‘baeldung’ on Google, then Kitty should see Eugen’s name in the results.

First, define tasks that Kitty can perform.

  1. Kitty can use Google:
    public class StartWith implements Task {
    
        public static StartWith googleSearchPage() {
            return instrumented(StartWith.class);
        }
    
        GoogleSearchPage googleSearchPage;
    
        @Step("{0} starts a google search")
        public <T extends Actor> void performAs(T t) {
            t.attemptsTo(Open
              .browserOn()
              .the(googleSearchPage));
        }
    }
  2. Kitty can make a search on Google:
    public class SearchForKeyword implements Task {
    
        @Step("{0} searches for '#keyword'")
        public <T extends Actor> void performAs(T actor) {
            actor.attemptsTo(Enter
              .theValue(keyword)
              .into(GoogleSearchPage.SEARCH_INPUT_BOX)
              .thenHit(Keys.RETURN));
        }
    
        private String keyword;
    
        public SearchForKeyword(String keyword) {
            this.keyword = keyword;
        }
    
        public static Task of(String keyword) {
            return Instrumented
              .instanceOf(SearchForKeyword.class)
              .withProperties(keyword);
        }
    }
  3. Kitty can see Google search results:
    public class GoogleSearchResults implements Question<List<String>> {
    
        public static Question<List<String>> displayed() {
            return new GoogleSearchResults();
        }
    
        public List<String> answeredBy(Actor actor) {
            return Text
              .of(GoogleSearchPage.SEARCH_RESULT_TITLES)
              .viewedBy(actor)
              .asList();
        }
    }

Also, we have already defined the Google search PageObject:

@DefaultUrl("https://www.google.com/ncr")
public class GoogleSearchPage extends PageObject {

    public static final Target SEARCH_RESULT_TITLES = Target
      .the("search results")
      .locatedBy("._ksh");

    public static final Target SEARCH_INPUT_BOX = Target
      .the("search input box")
      .locatedBy("#lst-ib");
}

Now our main test class would look like:

@RunWith(SerenityRunner.class)
public class GoogleSearchScreenplayLiveTest {

    @Managed(driver = "chrome") 
    WebDriver browser;

    Actor kitty = Actor.named("kitty");

    @Before
    public void setup() {
        kitty.can(BrowseTheWeb.with(browser));
    }

    @Test
    public void whenGoogleBaeldungThenShouldSeeEugen() {
        givenThat(kitty).wasAbleTo(StartWith.googleSearchPage());

        when(kitty).attemptsTo(SearchForKeyword.of("baeldung"));

        then(kitty).should(seeThat(GoogleSearchResults.displayed(), 
          hasItem(containsString("Eugen (Baeldung)"))));
    }
}

After running this test, we shall see screenshots of each step Kitty performed in the test report:

6. Summary

In this article, we have introduced how to use Screenplay Pattern with Serenity BDD. Also, with the help of PageObject, we don’t have to interact with WebDrivers directly, making our tests easier to read, maintain and extend.

For more details on PageObject and Screenplay Pattern in Serenity BDD, check out in the related section of the Serenity documentation.

As always, the full example code can be found over on the Github.

Go deeper into building a REST API with Spring:

>> CHECK OUT THE COURSE