Using Serenity with Cucumber, Part 3

Datetime:2016-08-23 01:14:56          Topic:          Share

This post follows on from the previous (seepart 1> andpart 2). Here I’ll close out this series of posts by performing a bit more abstraction and finish up with some thoughts about how and to what extent the Cucumber abstraction fits in.

Continuing on directly from the last post, we have a fully working implementation of a Cucumber feature file. This feature file is matched up to a series of step definitions which in turn delegate to a page object. In this post, I want to abstract away from the page object. But how? What I want to do is focus on a user who is acting out a series of steps.

Provide a Steps Library Reference

Let’s change our RecordTodoSteps like this:

package com.testerstories.tutorial.todos.features.steps;
 
import com.testerstories.tutorial.todos.pages.TodoPage;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import net.thucydides.core.annotations.Steps;
 
import static org.assertj.core.api.Assertions.assertThat;
 
public class RecordTodoSteps {
    @Steps
    Useruser;
    
    TodoPagetodoPage;
 
    @Given("^the todo application$")
    public void start_the_todo_application() {
        todoPage.open();
    }
 
    @When("^the todo action '(.*)' is added$")
    public void add_a_todo_action(String actionName) {
        todoPage.addActionCalled(actionName);
    }
 
    @Then("^'(.*)' should appear in the todo list$")
    public void action_should_appear_in_my_todo_list(String action) {
        assertThat(todoPage.getActions()).contains(action);
    }
}

Here I’m using the @Steps annotation. The @Steps annotation marks a Serenity step library.

In Serenity, the idea of a Step Library is to add a layer of abstraction between the intent and the implementation. I talked a little about this in my somewhat generic post on Java Automation with Serenity . This idea, however, takes on an interesting focus when you throw Cucumber into the mix. After all, the idea of Cucumber feature files — and their associated step definitions — already suggest such a separate of intent (“what”) from implementation (“how”).

So are just adding a superfluous layer here? In my view, not necessarily. The step library concept of Serenity can help you organize your overall code into more reusable components. This is the case regardless of whether Cucumber is in place or not.

With this current example, what I’m suggesting here is a move away from page objects and instead saying that Cucumber steps should be done in the context of a user’s actions. So, for that, I’ve established that there will be an instance of a User class. Where should this class go?

I’m going to put this in my src/main/java root package. Specifically I’ll create a steps package that is at the same level as the pages package we created in the previous post. In this package, create a User class. Here’s what your structure will look like:

Provide a Steps Library

To get ourselves going, let’s extend this User class from ScenarioSteps:

package com.testerstories.tutorial.todos.steps;
 
import net.thucydides.core.steps.ScenarioSteps;
 
public class User extends ScenarioSteps {
}

ScenarioSteps refers to a set of reusable steps for use in an acceptance test suite. The idea is that any class that extends the ScenarioSteps class calls the step library. So keep in mind what we have here. We have a test steps file (RecordTodoSteps) that has a particular instance (user) annotated with @Steps. That tells Serenity that this class is going to contain a series of steps that should be reported on as part of test execution. In effect, the User class is going to be the step library.

So let’s change our test class to start doing this. Change your @Given method statement as such:

    @Given("^the todo application$")
    public void start_the_todo_application() {
        user.opens_todo_application();
    }

Keep in mind what we’ve done here. This is important.

We changed the the code from todoPage.open() to user.opens_todo_application() . Is that a big deal? Well, sort of. It means we’re abstracting away from the page and more onto the user. If the user needs to delegate to the page, then that will be handled as part of the user actions. In fact, let’s create the opens_todo_application() method on the User class:

package com.testerstories.tutorial.todos.steps;
 
import com.testerstories.tutorial.todos.pages.TodoPage;
import net.thucydides.core.annotations.Step;
import net.thucydides.core.steps.ScenarioSteps;
 
public class User extends ScenarioSteps {
    TodoPagetodoPage;
 
    @Step
    public void opens_todo_application() {
        todoPage.open();
    }
}

Notice that here I’m referencing the page object (todoPage) in this class and I have a @Step annotated method that indicates that when this method is executed, it should be reported on in the test reporting. Keep in mind that the user class is a step library. That means it is composed of methods that are steps. You indicate which methods are steps by the @Step annotation.

Let’s replace our other statements in our test class so that they call methods on the user rather than on the page. In fact, here’s what the test class should look like (and I’ve removed imports when necessary):

package com.testerstories.tutorial.todos.features.steps;
 
import com.testerstories.tutorial.todos.steps.User;
import cucumber.api.java.en.Given;
import cucumber.api.java.en.Then;
import cucumber.api.java.en.When;
import net.thucydides.core.annotations.Steps;
 
public class RecordTodoSteps {
    @Steps
    Useruser;
 
    @Given("^the todo application$")
    public void start_the_todo_application() {
        user.opens_todo_application();
    }
 
    @When("^the todo action '(.*)' is added$")
    public void add_a_todo_action(String actionName) {
        user.adds_an_action_called(actionName);
    }
 
    @Then("^'(.*)' should appear in the todo list$")
    public void action_should_appear_in_my_todo_list(String action) {
        user.should_see_action_called(action);
    }
}

Notice that any references to the page object are now gone. This means the User class, with the above new methods created, should look like this:

package com.testerstories.tutorial.todos.steps;
 
import com.testerstories.tutorial.todos.pages.TodoPage;
import net.thucydides.core.annotations.Step;
import net.thucydides.core.steps.ScenarioSteps;
 
import static org.assertj.core.api.Assertions.assertThat;
 
public class User extends ScenarioSteps {
    TodoPagetodoPage;
 
    @Step
    public void opens_todo_application() {
        todoPage.open();
    }
 
    @Step
    public void adds_an_action_called(String actionName) {
        todoPage.addActionCalled(actionName);
    }
 
    @Step
    public void should_see_action_called(String action) {
        assertThat(todoPage.getActions()).contains(action);
    }
}

Notice that each method has a @Step annotation.

Go ahead and run this again. You should everything passes. In the test report, the steps for your scenario should show up like this:

The Abstractions

Yeah, that’s great and all … but haven’t I really just shuffled code around? In one sense, yes. But in another, very real sense, no. What I’ve done is moved the potentially more volatile code further down the stack. So my Cucumber feature file provides a certain level of intent, my test steps provide a certain level of implementation. That implementation is rooted in the actions of a user. That user delegates to a page object in this case, because that’s what is being tested. But that same user could be delegating down to a mobile emulator or to a REST service.

One thing I’ll say though is that the Cucumber abstraction still sticks out like a sore thumb to me. That’s mainly because it’s really just repeating what the code does. Consider my feature file and my code steps:

Given the todo application
    When  the todo action 'Digitize Supreme Power Collection' is added
    Then  'Digitize Supreme Power Collection' should appear in the todo list
user.opens_todo_application();
user.adds_an_action_called(actionName);
user.should_see_action_called(action);

If you replace the variable names in the code with their values, you see how close everything aligns:

user.opens_todo_application();
user.adds_an_action_called("Digitize Supreme Power Collection");
user.should_see_action_called("Digitize Supreme Power Collection");

So what I really want is my Cucumber features to be a bit more declarative. How about this?

Feature: Add new todos
  Users need to be able to quickly add tasks as fast as they can think of them.

  Scenario: Add a new todo
    * users can add an action to the todo list

Here I’ve essentially reduced the scenario to one line, that is very declarative in nature. With this in place, let’s change the RecordToDo file as such:

package com.testerstories.tutorial.todos.features.steps;
 
import com.testerstories.tutorial.todos.steps.User;
import cucumber.api.java.en.Given;
import net.thucydides.core.annotations.Steps;
 
public class RecordTodoSteps {
    @Steps
    Useruser;
 
    @Given("^users can add an action to the todo list$")
    public void user_adds_an_action_to_the_todo_list() {
        String action = "Digitize Supreme Power Collection";
 
        user.opens_todo_application();
        user.adds_an_action_called(action);
        user.should_see_action_called(action);
    }
}

Notice here that I didn’t have to change my User class at all nor did I have to change the page object, TodoPage.

If you run this, you’ll find the test report looks like this:

Pull Down vs Push Up

While this is admittedly a very simple example, I do think it showcases exactly how you want to think about “pulling down English” versus “pushing up English”. Notice, however, that to do this I had to use the “*” operator in Gherkin, which effectively just means “any step.” Not all BDD tools support this. JBehave, for example, does not. This is where I think Gherkin can becoming constricting.

Contrast some of this, incidentally, with the logic I presented in my Screenplay Serenity posts. At one point, you’ll end up with code like this:

givenThat(jeff).wasAbleTo(StartWith.anEmptyTodoList());
when(jeff).attemptsTo(AddATodoItem.called("Digitize JLA vol 1 collection"));
then(jeff).should(seeThat(TodoItemsList.displayed(), hasItem("Digitize JLA vol 1 collection")));

Yes, it’s “busier” than pure English would be but, on the other hand, as we’ve just seen I can keep my English fairly constrained by sticking just to the business rules. Instead of trying to “modularize” at the level of natural language, I modularize at the code level, where that kind of practice belongs.

Further, it allows me to push English back up, so that if people do want the implementation details, the automation can serve not just as an execution mechanism but also as a documentation generator.

How Business Speaks

To drive this home a bit, here’s an example of how most business teams I work with want to describe features and scenarios.

Doctors provide information about medical conditions to patients via targeted articles.
Patients need to be able to access those articles conveniently.

Patients must be able to print an article
Patients must be able to reprint an article
Patients must be able to send an article link to an email address

Doctors must be able to view activity on the article
Doctors must be able to deactivate an article
Doctors must be able to edit provider and location for a scheduled article

This, to me, is perfect. It’s exactly what I want from product and business teams. A statement of why this overall feature adds value. Then a breakdown of how it adds value by talking about a high-level interpretation of what the feature does to provide value. It’s now up to developers and testers to work out the logistics of what exactly the solution will look like.

The development code and test code should be developed in tandem. This way both sets of code become a common specification and thus a single source of truth. This is somewhat in line with some of my thinking frommodern testing.

Notice, too, that the concept of specific users can be directly translated from those business requirements into a particular type of class (DoctorUser, PatientUser). Even better, if you go with the Screenplay approach that I talked about in previous posts, you can treat Doctor and Patient as particular types of Actor instances.

This gives you a lot more flexibility to change based on development spikes or the product team refining its understanding. How you manage that flexibility is the effective use of abstraction layers. You’ll notice that, in this post, that meant going towards a very minimal implementation of a “Gherkin” style specification.

To Cuke Or Not To Cuke

I’ll close off here with this final consideration.

Ask yourself this: why are “Gherkin-like” approaches chosen? Usually because those are “easier” to automate. But it’s not necessarily because it’s easier to speak in. People don’t “speak Gherkin.” The above example of Patient/Doctor was plenty good enough to spark further conversations and collaborations. And you’ll notice not one bit of that is in Gherkin.

So while the posts in this series have focused on the integration of Cucumber and Serenity, I want to leave you with the thought that you should, at minimum, consider the level of expression in terms of how you use Cucumber. Beyond that I would hope you consider whether or not Cucumber, and the focus on Gherkin-style feature files, is even the most effective or efficient approach.