Cucumber JVM Creating a custom plugin

Introduction

What to do when existing Cucumber plugins do not meet ones requirements? Cucumber allows custom plugins to be created and added to the runner. The steps for accomplishing this is detailed in this article. This is applicable to Cucumber version 4.0.0 and greater. A brief explanation of how to deal with previous versions can be found at the end.

Plugin Creation

The custom plugin needs to implement one of cucumber.api.event.EventListener or cucumber.api.event.ConcurrentEventListener interface. Else one can also implement the cucumber.api.StepDefinitionReporter and/or cucumber.api.SummaryPrinter. These in turn extend the cucumber.api.Plugin interface.

This custom plugin needs to have a no-argument constructor if passing argument is optional or none are required. To pass an argument a single-argument constructor needs to be added. The argument passed to the single-argument constructor needs to be one of these classes – java.lang.String, java.lang.Appendable, java.net.URI, java.net.URL or java.io.File. There can be only one single-argument constructor.

public class CustomPlugin implements EventListener {

    public CustomPlugin() { }

    public CustomPlugin(File file) { }
}

The most useful interfaces to implement are the EventListener or ConcurrentEventListener which have callback methods that need to be implemented in the custom plugin class for execution events. The CustomPlugin class above has both constructors which allows for the case of optional argument. Depending on ones requirements both or only one constructor may be sufficient.

Plugin Addition

The custom plugin needs to be mentioned, along with the fully qualified class name, in plugins option of the CucumberOptions annotation on the runner class. There can be multiple plugins mentioned in this, including custom plugins. Below is an example with no arguments, other options are not shown.

@CucumberOptions(plugin = {"formatter.CustomPlugin"})

Below is an example in which an argument is mentioned. This argument will be passed onto the single argument constructor. The ‘:’ delimiter is used to separate the class and the argument.

@CucumberOptions(plugin = {"formatter.CustomPlugin:run.log"})

This is all that is required to implementing a custom plugin.

EventListener & ConcurrentEventListener

When the custom plugin needs to respond to cucumber events then these two interfaces are the best choice. These interfaces have one method setEventPublisher which takes a cucumber.api.event.EventPublisher as an argument which need to be implemented. In this method an event handler is registered for each event that one is interested in listening to.

The example below uses Java8 method references for all the event handlers, which may not be required in all cases. Otherwise one can create an inline object for the cucumber.api.event.EventHandler interface and pass the event to the receive() method. Details about the events are added above the respective register lines to the publisher.

public void setEventPublisher(EventPublisher publisher) {
    
    //Event sent when test run in started. TestRunStarted event is sent to handler.
    //TestRunStarted contains the timestamp of the test run start.
    publisher.registerHandlerFor(TestRunStarted.class, this::handleRunStarted);
    
    //Event sent when feature file is read. TestSourceRead event is sent to handler.
    //TestSourceRead contains the location of the feature file and its contents.
    publisher.registerHandlerFor(TestSourceRead.class, this::handleSourceRead);  
    
    //Event sent before scenario execution. TestCaseStarted event is sent to handler.
    //TestCaseStarted contains the scenario details like uri, line, steps, tags etc. 
    publisher.registerHandlerFor(TestCaseStarted.class, this::handleCaseStarted);
    
    //Event sent before step execution. TestStepStarted event is sent to the handler.
    //TestStepStarted contains step details.
    publisher.registerHandlerFor(TestStepStarted.class, this::handleStepStarted);
    
    //Event sent after step execution. TestStepFinished event is sent to the handler.
    ////TestStepFinished contains step details and result of the step.
    publisher.registerHandlerFor(TestStepFinished.class, this::handleStepFinished);

    //Event sent after scenario execution. TestCaseFinished event is sent to handler.
    //TestCaseFinished contains the scenario details and test case result.
    publisher.registerHandlerFor(TestCaseFinished.class, this::handleCaseFinished);
    
    //Event sent when test run in finished. TestRunFinished event is sent to handler.
    //TestRunFinished contains the timestamp of the test run end.
    publisher.registerHandlerFor(TestRunFinished.class, this::handleRunFinished);
    
    //Event sent when scenario.embed is called inside a hook. EmbedEvent is sent to the handler.
    publisher.registerHandlerFor(EmbedEvent.class, this::handleEmbedEvent);
    
    //Event sent when scenario.write is called inside a hook. WriteEvent is sent to the handler.
    publisher.registerHandlerFor(WriteEvent.class, this::handleWriteEvent);
    
    //Event sent when step cannot be matched to a step definition. SnippetsSuggestedEvent is sent to handler.
    publisher.registerHandlerFor(SnippetsSuggestedEvent.class, this::handleSnippetSuggest);
}

ConcurrentEventListener is used when one requires events to be relayed in real time. When this is used in case of parallel execution. the plugin needs to take care of management of the different threads. In case of EventListener the events are emitted after the completion of the test run. The events are sorted in canonical order (cucumber.api.event.CanonicalEventOrder) and then emitted to the plugin.

Previous versions

The difference between the versions is only what interfaces need to be implemented by the custom plugin class. The process of adding the custom plugin to the cucumber.api.CucumberOptions annotation remains the same.

For cucumber version upto 1.2.5, the custom plugin needs to implement the gherkin.formatter.Formatter and/or gherkin.formatter.Reporter interface. The Formatter interface deals mainly with events relating to the feature file while the Reporter interface contains the results of the execution. These two are the most useful interfaces to implement. One can also use the cucumber.api.StepDefinitionReporter and/or cucumber.api.SummaryPrinter interfaces.

For cucumber version from 2.0.0 onwards till 3.0.2, the custom plugin needs to implement the cucumber.api.formatter.Formatter marker interface, which in turn extends the cucumber.api.event.EventListener interface. Thus one will need to implement the setEventPublisher method similar to above.

Leave a Reply

Your email address will not be published. Required fields are marked *