All you need to know about Serenity BDD Step Libraries

John Ferguson Smart | Mentor | Author | Speaker - Author of 'BDD in Action'.
Helping teams deliver more valuable software sooner25th September 2017

In Serenity 1.6.0, we have refactored how step libraries are managed, to make them more intuitive and easier to understand.

Basic step libraries

In Serenity, we use step libraries to better organise our test logic into reusable components.

Step libraries are often used to represent actors or persona who interact with the application. For example, we might have an AccountHolder step library that represents how a client interacts with a banking application to open and manage her account.

public static class AccountHolder {

    private long newBankAccountNumber = 0;

    /**
     * A client opens a new bank account via the client website.
     * We record the bank account number for future use
     */
    @Step
    public void opensABankAccount() {...}

    /**
     * Does this client have an open account?
     */
    public boolean hasAnOpenAccount() { ... }

    /**
     * What is the new bank account number for this customer?
     */
    public long newBankAccountNumber() {
        return newBankAccountNumber;
    }
}

Methods that represent a business task or action (opensABankAccount()), and that will appear in the reports as a separate step, are annotated with the @Step annotation. Other methods, such as hasAnOpenAccount(), query the state of the application and are used in assert statements; these ones don't need to have the @Step annotation.

We could use this step library to write a test that illustrates a user opening a new account online like this:


@RunWith(SerenityRunner.class)
public void WhenACustomerOpensANewAccount {

    @Steps
    AccountHolder jane;

    @Test
    public void jane_opens_an_account() {
        jane.opensABankAccount();

        assertThat(jane.hasAnOpenAccount(), is(true));
    }

We could make this a little more declarative by giving our account holder a name

public class AccountHolder {

    private long newBankAccountNumber = 0;
    public String firstName;
    public String lastName;

    @Step("Given an account holder called {0} {1}")
    public void isCalled(String firstName, String lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }
    ...
}

Now we can give Jane some details that can be used during the account creation process:

@Test
public void jane_opens_an_account() {
    jane.isCalled("Jane","Smith");

    jane.opensABankAccount();

    assertThat(jane.hasAnOpenAccount(), is(true));
}

We might want to flesh out the details of the account creation process, and break it down into steps, like "choose to open an account", "provide personal details" and "apply for a current account". We would add @Step-annotation methods for each of these tasks:

@Test
public void jane_opens_an_account() {
    jane.isCalled("Jane","Smith");

    jane.choosesToOpenABankAccount();
    jane.providesPersonalDetails();
    jane.appliesForACurrentAccount();

    assertThat(jane.hasAnOpenAccount(), is(true));
}

Each of these steps might use other steps (or even other step libraries), or interact with the web application via a page object. We might end up with a providesPersonalDetails() method that looks like this:

AccountApplicationPage accountApplicationPage;
@Step
public void providesPersonalDetails() {
    accountApplicationPage.enterCustomerName(firstName, lastName);
    accountApplicationPage.enterDateOfBirth(dateOfBirth);
    accountApplicationPage.enterAddress(address);
    ...
}

Using several step libraries

Sometimes we can use several step libraries of the same type to make our tests more readable. For example, the following test shows how bank transfers between different customers works.

@Steps
AccountHolder jane;

@Steps
AccountHolder joe;

@Test
public void jane_transfers_money_to_joe() {
    jane.isCalled("Jane","Smith");
    joe.isCalled("Joe","Blogs");

    jane.hasACurrentAccountWithABalanceOf(1000.00);
    joe.hasACurrentAccountWithABalanceOf(100.00);

    jane.transfersTo(joe.getAccountNumber(), 100.00);

    assertThat(jane.getAccountBalance(), is(900.00));
    assertThat(joe.getAccountBalance(), is(200.00));
}

Note that a more elegant way to do this is by using the Screenplay pattern, where each actor can have their own browser and abilities.

Shared Instances

There are some cases where we want to reuse the same step library instance in different places across a test. For example, suppose we have a step library that interacts with a backend API, and that maintains some internal state and caching to improve performance. We might want to reuse a single instance of this step library, rather than having a separate instance for each variable.

We can do this by declaring the step library to be shared, like this:

@Steps(shared = true)
CustomerAPIStepLibrary customerAPI;

Now, any other step libraries of type CustomerAPIStepLibrary, that have the shared attribute set to true will refer to the same instance.

In older versions of Serenity, sharing instances was the default behaviour, and you used the uniqueInstance attribute to indicate that a step library should not be shared. If you need to force this behaviour for legacy test suites, set the step.creation.strategy property to legacy in your serenity.properties file:

step.creation.strategy = legacy

Have fun!

Related courses and workshops

If you want to learn more about Serenity BDD, take a look at the Serenity Dojo online training programme. The Serenity Dojo Online Training Programme is an innovative and exciting way of learning good BDD and Test Automation practices, including CucumberSelenium WebDriver, and Serenity BDD, and also the advanced Java development skills you need to be a great test automation specialist.

© 2019 John Ferguson Smart