Filed under: Cucumber, — Tags: Cucumber-JVM, Plugin — Thomas Sundberg — 2019-03-07
Cucumber-JVM comes with some built in plugins. They are mostly used for reporting. If the built in plugins aren't enough for your needs, it is possible to implement your own plugin.
This blog post will show you how to
The first step is to create a class that implements one of the interfaces
cucumber.api.event.EventListener
cucumber.api.event.ConcurrentEventListener
Both define the method void setEventPublisher(EventPublisher publisher);
. The difference is that events sent
to cucumber.api.event.ConcurrentEventListener
are sent from a publisher that is synchronized. The documentation says that
"Events are not published concurrently".
If you don't have any specific reason for not being thread safe, implement cucumber.api.event.ConcurrentEventListener
.
What you need to do is to register callbacks so the proper events can be received. Each receiver must
implement the interface cucumber.api.event.EventHandler<T extends Event>
A solution that is common in the plugins built in into Cucumber-JVM is to create an implementation of the interface and delegate to an instance method.
The built in plugin cucumber.runtime.formatter.JSONFormatter
declares an instance variable like this:
private EventHandler<TestSourceRead> testSourceReadHandler = new EventHandler<TestSourceRead>() {
@Override
public void receive(TestSourceRead event) {
handleTestSourceRead(event);
}
};
This instance variable delegates to a method called handleTestSourceRead(event);
implemented in the same class.
The handlers are then registered in the method public void setEventPublisher(EventPublisher publisher)
. The cucumber.runtime.formatter.JSONFormatter
looks like this:
@Override
public void setEventPublisher(EventPublisher publisher) {
publisher.registerHandlerFor(TestSourceRead.class, testSourceReadHandler);
publisher.registerHandlerFor(TestCaseStarted.class, caseStartedHandler);
publisher.registerHandlerFor(TestStepStarted.class, stepStartedHandler);
publisher.registerHandlerFor(TestStepFinished.class, stepFinishedHandler);
publisher.registerHandlerFor(WriteEvent.class, writeEventhandler);
publisher.registerHandlerFor(EmbedEvent.class, embedEventhandler);
publisher.registerHandlerFor(TestRunFinished.class, runFinishedHandler);
}
There is a total of seven different events that cucumber.runtime.formatter.JSONFormatter
listens for.
There is a total of eleven different events that can be registered. They are:
cucumber.api.event.Event
- all events.cucumber.api.event.TestRunStarted
- the first event sent.cucumber.api.event.TestSourceRead
- sent for each feature file read, contains the feature file source.cucumber.api.event.SnippetsSuggestedEvent
- sent for each step that could not be matched to a step definition, contains the raw snippets for the step.cucumber.api.event.TestCaseStarted
- sent before starting the execution of a Test Case(/Pickle/Scenario), contains the Test Casecucumber.api.event.TestStepStarted
- sent before starting the execution of a Test Step, contains the Test Stepcucumber.api.event.EmbedEvent
- calling scenario.embed in a hook triggers this event.cucumber.api.event.WriteEvent
- calling scenario.write in a hook triggers this event.cucumber.api.event.TestStepFinished
- sent after the execution of a Test Step, contains the Test Step and its Result.cucumber.api.event.TestCaseFinished
- sent after the execution of a Test Case(/Pickle/Scenario), contains the Test Case and its Result.cucumber.api.event.TestRunFinished
- the last event sent.Plugins are configured through their constructor.
Create either a default constructor or a constructor that takes an argument. The constructor that takes an argument is used to configure the plugin. The possible types are
java.net.URI
java.net.URL
java.io.File
java.lang.String
java.lang.Appendable
The proper constructor will be located by cucumber.runtime.formatter.PluginFactory
and their constructor that accepts a String
will be used.
The constructor for JSONFormatter
looks like this:
public JSONFormatter(Appendable out) {
this.out = new NiceAppendable(out);
}
An example may look like this:
package se.thinkcode.report;
import cucumber.api.event.ConcurrentEventListener;
import cucumber.api.event.EventHandler;
import cucumber.api.event.EventPublisher;
import cucumber.api.event.TestRunStarted;
import cucumber.runtime.CucumberException;
import java.io.File;
public class ReportThingy implements ConcurrentEventListener {
private File reportDir;
public ReportThingy(String outputDir) {
createOutputDir(outputDir);
}
private void createOutputDir(String outputDir) {
reportDir = new File(outputDir);
if (!reportDir.exists() && !reportDir.mkdirs()) {
throw new CucumberException("Failed to create dir: " + outputDir);
}
}
private EventHandler<TestRunStarted> runStartedHandler = new EventHandler<TestRunStarted>() {
@Override
public void receive(TestRunStarted event) {
startReport(event);
}
};
@Override
public void setEventPublisher(EventPublisher publisher) {
publisher.registerHandlerFor(TestRunStarted.class, runStartedHandler);
}
private void startReport(TestRunStarted event) {
System.out.println(event.getTimeStamp());
}
}
How do you use the plugin then? When you use one of the built in plugins, you can declare it like this:
@CucumberOptions(
plugin = "pretty"
)
A custom plugin is declared with its complete name like this:
@CucumberOptions(
plugin = "se.thinkcode.report.ReportThingy:./build/cucumber/report"
)
If you use the command line then you would declare it as
--plugin se.thinkcode.report.ReportThingy:./build/cucumber/report
The part to the right of the colon is the configuration, in this case ./build/cucumber/report
which is a directory that the plugin should use.
Implementing a Cucumber plugin is a four-step process
cucumber.api.event.EventListener
or cucumber.api.event.ConcurrentEventListener
I would like to thank Malin Ekholm and Mika Kytöläinen for feedback.