Skip to content

Instantly share code, notes, and snippets.

@ekalin
Created February 6, 2019 17:01
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save ekalin/612f62d96d936c20f69bff15dbf9e03b to your computer and use it in GitHub Desktop.
Save ekalin/612f62d96d936c20f69bff15dbf9e03b to your computer and use it in GitHub Desktop.
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(Cucumber.java) 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 Cucumber.java 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;
import java.util.stream.Stream;
/**
* 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 {
super(clazz);
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);
}
@Override
protected List<FeatureRunner> getChildren() {
return cucumberRunner.getChildren();
}
@Override
protected Description describeChild(FeatureRunner child) {
return child.getDescription();
}
@Override
protected void runChild(FeatureRunner child, RunNotifier notifier) {
child.run(notifier);
}
@Override
protected Statement childrenInvoker(RunNotifier notifier) {
try {
Method method = cucumberRunner.getClass().getDeclaredMethod("childrenInvoker", RunNotifier.class);
method.setAccessible(true);
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.
*/
@SuppressWarnings("unchecked")
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);
}
f.setAccessible(true);
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