Skip to content

Instantly share code, notes, and snippets.

What would you like to do?
Per-Runner JSON reports with Cucumber

When using multiple Cucumber runners, sharing a common configuration via @CucumberOptions in a base class, and running all tests with mvn verify or equivalent, the JSON report will only have the results of the features of the last runner.

This code provides a workaround. Replace @RunWith( with @RunWith(IndividualJsonCucumber.class), and each runner will generate its own JSON report inside target/cucumber-reports, in a file matching the name of the Java runner class (including its directory).

This solution is far from elegant due to use of reflection, but since it delegates all the work to Cucumber, it should keep working even if a newer version of introduces changes.

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import cucumber.runtime.junit.FeatureRunner;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.InitializationError;
import org.junit.runners.model.Statement;
import java.lang.annotation.Annotation;
import java.lang.reflect.Field;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.List;
import java.util.Map;
* A {@link Cucumber} runner that outputs one JSON report per run.
* <p>
* Useful in cases where there are multiple runners (possibly sharing a common configuration), and when they are all run
* (with mvn verify or similar).
* <p>
* Currently the JSONs are generated in the hard-coded directory {@code target/cucumber-reports}, in files named after
* the runner class.
public class IndividualJsonCucumber extends ParentRunner<FeatureRunner> {
private final Cucumber cucumberRunner;
public IndividualJsonCucumber(Class<?> clazz) throws InitializationError {
CucumberOptions cucumberOptions = getCucumberOptions(clazz);
String[] plugin = cucumberOptions.plugin();
String[] newPlugins = setJsonPlugin(plugin, jsonPlugin(clazz));
changeAnnotationValue(cucumberOptions, "plugin", newPlugins);
cucumberRunner = new Cucumber(clazz);
private CucumberOptions getCucumberOptions(Class<?> clazz) {
CucumberOptions annotation = clazz.getAnnotation(CucumberOptions.class);
if (annotation != null) {
return annotation;
Class<?> superclass = clazz.getSuperclass();
if (superclass == Object.class) {
return null;
return getCucumberOptions(superclass);
private String jsonPlugin(Class<?> clazz) {
String name = clazz.getName();
String path = name.replace(".", "/") + ".json";
return "json:target/cucumber-reports/" + path;
private String[] setJsonPlugin(String[] original, String newElt) {
Stream<String> originalNonJson = Stream.of(original).filter(p -> !p.startsWith("json"));
return Stream.concat(originalNonJson, Stream.of(newElt)).toArray(String[]::new);
protected List<FeatureRunner> getChildren() {
return cucumberRunner.getChildren();
protected Description describeChild(FeatureRunner child) {
return child.getDescription();
protected void runChild(FeatureRunner child, RunNotifier notifier) {;
protected Statement childrenInvoker(RunNotifier notifier) {
try {
Method method = cucumberRunner.getClass().getDeclaredMethod("childrenInvoker", RunNotifier.class);
return (Statement) method.invoke(cucumberRunner, notifier);
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
throw new IllegalStateException(e);
* Changes the annotation value for the given key of the given annotation to newValue and returns
* the previous value.
private static void changeAnnotationValue(Annotation annotation, String key, Object newValue) {
Object handler = Proxy.getInvocationHandler(annotation);
Field f;
try {
f = handler.getClass().getDeclaredField("memberValues");
} catch (NoSuchFieldException | SecurityException e) {
throw new IllegalStateException(e);
Map<String, Object> memberValues;
try {
memberValues = (Map<String, Object>) f.get(handler);
} catch (IllegalArgumentException | IllegalAccessException e) {
throw new IllegalStateException(e);
Object oldValue = memberValues.get(key);
if (oldValue == null || oldValue.getClass() != newValue.getClass()) {
throw new IllegalArgumentException();
memberValues.put(key, newValue);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment