Sharing state between steps in Serenity BDD

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

Whether you are using Cucumber, JBehave or just JUnit, Serenity BDD encourages a layered, structured approach to automation. The reason for this is simply that it makes the tests easier to understand and maintain, and faster to write in the medium term. But people often wonder what is the best approach to share information between steps. In this article, we look at one way to do this with Serenity BDD.

One of the main ways to organise and layer the tests is to break each test into a sequence of business-focused tasks. In Cucumber or JBehave, for example, we start off with a description of the scenario in business terms:

Scenario: View only completed items
  Given that Jane has a todo list containing Buy some milk, Walk the dog
  And she has completed the task called 'Walk the dog'
  When she filters her list to show only Completed tasks
  Then her todo list should contain Walk the dog

Each line in these scenarios maps to a "step definition" method (as shown below for Cucumber). These step definitions act as "glue" between the text in the scenario and the code that implements the corresponding test logic. In classic Serenity BDD, this test logic is placed in reusable methods, which live in classes that we call "step libraries".

public class TodoListStepDefinitions {

    @Steps
    TodoUserSteps user;

    @Given("that (.*) has a todo list containing (.*)")
    public void startWithATodoList(String userName, List<String> tasks) {
        user.connects_to_the_application_as(userName);
        user.starts_with_a_todo_list_containing(tasks);
    }

    @Given("she has completed the task called '(.*)'")
    public void completeTask(String task) {
        user.completes(task);
    }
}

If we are using Serenity with JUnit (and we are not using the Screeplay pattern), we do something similar, but without the step definition methods. So for the same test, we might have something like this:

@Steps 
TodoUserSteps jane;

@Test
public void should_be_able_to_view_only_completed_todos_with_page_objects() {

    jane.connects_to_the_application_as("Jane Smith");

    jane.starts_with_a_todo_list_containing("Walk the dog", "Put out the garbage");

    jane.completes("Walk the dog");

    jane.filters_items_to_show(TodoStatusFilter.Completed);

    jane.should_see_that_displayed_items_contain("Walk the dog");
    jane.should_see_that_displayed_items_do_not_contain("Put out the garbage");
    jane.should_see_that_the_currently_selected_filter_is(TodoStatusFilter.Completed);
}

Here, the TodoUserSteps class contains the methods for the steps that Jane can do. The problem with this approach is that this class can become large and unwieldy. You can easily end up with a single big class with a lot of methods that do lots of different things, which can make maintaining the test code a hassle further down the track.

One way to avoid bloat in the step libraries is to break them down by role. For example, we could have a step library for when Jane connects to the application, and others when she performs specific tasks on the todo items.

@Steps 
ConnectToApplicationSteps jane_connects_to_the_application;

@Steps 
CompleteTaskSteps jane_completes;

@Steps 
FilterTaskSteps jane_filters;

@Steps 
AssertionTaskSteps jane_should_see;

@Test
public void should_be_able_to_view_only_completed_todos_with_page_objects() {

    jane_connects_to_the_application.as("Jane Smith");

    jane.starts_with_a_todo_list_containing("Walk the dog", "Put out the garbage");

    jane_completes.a_task_called("Walk the dog");

    jane_filters.items_to_show(TodoStatusFilter.Completed);

    jane_should_see.that_displayed_items_contain("Walk the dog");
    jane_should_see.that_displayed_items_do_not_contain("Put out the garbage");
    jane_should_see.that_the_currently_selected_filter_is(TodoStatusFilter.Completed);
}

This way, the step libraries become smaller, more focused, and easier to maintain.

(A more elegant solution would be to use the Screenplay pattern, but this might involve a lot of rework for already-established code bases).

A question that often comes up with this approach is how to keep track of information between steps in a test. Serenity gives you a simple way to do this, that works with all of the supported frameworks.

When you use the @Steps annotation for a field, as in the examples above, Serenity will instantiate the field with an instance of the class. Unless you tell Serenity otherwise, this instance will be shared across the whole test.

In other words, when a test is running, a step library field (the ones annotated with the @Steps annotation) for a given class will refer to the same instance no matter where it is called.

For example, suppose you needed to keep track of the name of the current user. You can use the Serenity session hash map to do this, e.g.

Serenity.setSessionVariable("User Name").to("Jane");

and later on, in another step:

String currentUserName = Serenity.sessionVariableCalled("User Name");

But this means you need to keep track of the session variable names, which can become cumbersome if you have a lot of them. You could also simply store this value in the ConnectToApplicationSteps class, and reuse this class elsewhere. The ConnectToApplicationSteps class might look like this:

public class ConnectToApplicationSteps {
    TodoListPage todoListPage;

    String currentUserName;

    public String getUserName() {
        return currentUserName;
    }

    @Step("{0} connects to the application")
    public void as(String username) {
        todoListPage.openApplication();
        currentUserName = username;
    }
}

Other step classes could then refer to the to the ConnectToApplicationSteps to reference this information:

public class AssertionTaskSteps {

    @Steps
    ConnectToApplicationSteps currentUser;
    TodoListPage todoListPage;

    @Step("The connected user name should be visible")
    public void see_that_the_connected_user_name_is_visible() {
        String connectedUsername = currentUser.getUserName();
        AssertThat(todoListPage.getConnectedUserName(), equalTo(connectedUsername));
    }
}

This approach allows a more structured and better organised approach to sharing data between steps, and works well in JUnit, Cucumber and JBehave. The code discussed here was tested with Serenity 1.5.5.

© 2019 John Ferguson Smart