John Ferguson Smart

John's latest articles

Creating Tasks in Serenity Screenplay

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

November 5, 2018

Serenity Screenplay helps us write cleaner, more elegant and more maintainable code, and Serenity 2.0.13 has added some great improvements in this area. Read on to find out more!

One of the most powerful aspects of Serenity Screenplay is the way it documents user interactions with the system in a clean and readable way, both within the code, and in the Serenity reports. For example, the following test code is both readable and reportable:

   @Test
   public void the_fruit_should_be_norrishing() {

       Actor freddy = Actor.named("Freddy").describedAs("A fruit lover");

       givenThat(freddy).wasAbleTo(Peel.anApple());

       when(freddy).attemptsTo(Eat.anApple(),
                               Eat.aLargePear());

       then(freddy).should(seeThat("Freddy is not hungry", isNotHungry()));
   }

In the Serenity reports, each of these steps will appear:

In Serenity Screenplay, we usually want a task to appear in a report. They correspond to a logical business action or unit of user behaviour, and we want to see them as an integral part of our living documentation. But sometimes you don’t. In this article, we will look at some of the different ways you can create new tasks, and how this affects whether they will appear in your reports.

Creating tasks with simple constructors

Serenity uses instrumentation to report on task activity. Any instrumented task will always appear in the reports. You can instrument a class explicity, using the Tasks.instrumented(). So if we have a class like the following:

   public class EatsAnApple implements Performable {
      @Override
      public <T extends Actor> void performAs(T actor) {...}
   }

Then you can create this class using the instrumented() method like this:

   Performable eatsAnApple = Tasks.instrumented(EatsAnApple.class);

   freddy.attemptsTo(eatAnApple);

Using builders and factory classes

More complex tasks may have parameters to fine-tune their behavior. For example, in the following task, we refine the reporting to include the size of the fruit:

class EatsAPear implements Performable {

    private final String size;

    public EatsAPear(String size) {
        this.size = size;
    }

    @Override
    @Step("{0} eats a #size pear")
    public  void performAs(T actor) {}

    static EatsAPear ofSize(String size) {
        return instrumented(EatsAPear.class, size);
    }

}

The ofSize() method creates an instrumented instance of the EatsAPear class, with the right size value. We could write a Factory class to build a more fluent DSL like the one we saw at the start of the article:

   public static class Eat {
       public static Performable anApple() { return new EatsAnApple(); }
       public static Performable aLargePear() { return EatsAPear.ofSize("large"); }
       public static Performable aSmallPear() { return EatsAPear.ofSize("small"); }
   }

But with Serenity 2.0.13, this code can be simplified. If we add a default constructor to our performable class, we can avoid having to instrumente the class at all:

class EatsAPear implements Performable {

    private String size;

    // Needed by Serenity
    public EatsAPear() {}
    
    public EatsAPear(String size) {
        this.size = size;
    }

    @Override
    @Step("{0} eats a #size pear")
    public  void performAs(T actor) {}

    static EatsAPear ofSize(String size) {
        return instrumented(EatsAPear.class, size);
    }
}

Now we can use the parameterised constructors directly in our factory class, like this:

   public static class Eat {
       public static Performable anApple() { return new EatsAnApple(); }
       public static Performable aLargePear() { return new EatsAPear("large"); }
       public static Performable aSmallPear() { return new EatsAPear("small"); }
   }

Silent Tasks

But sometimes, you might want to not have a task appear in a report, but still use a Screenplay approach. For example, you may want a task that appears when it is called in the middle of a test, but not when the same task is called when preparing the test data.

To achieve this, we make our task implement the CanBeSilent interface. This interface has a single method, isSilent(), which determines whether the task should appear in the report:

class EatsAWatermelon implements Performable, CanBeSilent {

    private final boolean isSilent;
    private final String fruit;

    EatsAWatermelon(boolean isSilent, String fruit) {
        this.isSilent = isSilent;
        this.fruit = fruit;
    }

    public static EatsAWatermelon quietly() {
        return Tasks.instrumented(EatsAWatermelon.class,true, "watermelon quietly");
    }

    public static EatsAWatermelon noisily() {
        return Tasks.instrumented(EatsAWatermelon.class,false, "watermelon loudly");
    }

    @Override
    @Step("{0} eats a #fruit")
    public <T extends Actor> void performAs(T actor) {}

    @Override
    public boolean isSilent() {
        return isSilent;
    }
}

So if our test had the following code:

Actor.named("Annie").attemptsTo(EatsAWatermelon.quietly(), EatsAWatermelon.noisily());

Then only “Annie eats a watermelon loudly” would appear in the reports.

If you have a task class that you never want instrumented (for example, one that sets up some background test data), just add the  IsSilent interface – this is a marker interface with no methods, so adding it to your class declaration is enough:

class EatsAWatermelon implements Performable, IsSilent {
    ...
}

Conclusion

Serenity Screenplay gives you a great deal of flexibility in how you write your Performable classes, and whether you want them to appear in the report or not. And in Serenity 2.0.13, you no longer need to explicitly instrument Screenplay tasks.

We cover this topic in more detail in the latest module of the Serenity Dojo Screenplay course, part of the Serenity Dojo Online Curriculum that you can find here. If you haven’t seen the Screenplay course yet, you can check it out here – the first module is free!