Cucumber-JVM is a framework for writing end to end tests in natural language, with each line backed by a Java method. Each Java method has a regular expression of natural language lines to match, and lines should only match one such pattern. On a recent assignment I was tasked with modernizing a fairly large cucumber test suite, and going through the steps I found a lot of Java methods that were not being called from the natural language feature files anymore. To identify and remove these unused steps, and prevent any new unused steps in the future, I contributed the following plugin to Cucumber 4.4.0 through: - -

import java.util.Map;
import java.util.Map.Entry;
import java.util.TreeMap;

import cucumber.api.SummaryPrinter;
import cucumber.api.event.*;
import cucumber.api.formatter.NiceAppendable;

public class UnusedStepsSummaryPrinter implements EventListener, SummaryPrinter {

	private final Map<String, String> unusedSteps = new TreeMap<>();
	private final NiceAppendable out;

	@SuppressWarnings("WeakerAccess") // Used by PluginFactory
	public UnusedStepsSummaryPrinter(Appendable out) {
		this.out = new NiceAppendable(out);

	public void setEventPublisher(EventPublisher publisher) {
		// Record any steps registered
		publisher.registerHandlerFor(StepDefinedEvent.class, (event) -> unusedSteps.put(event.stepDefinition.getLocation(false), event.stepDefinition.getPattern()));
		// Remove any steps that run
		publisher.registerHandlerFor(TestStepFinished.class, (event) -> {
			String codeLocation = event.testStep.getCodeLocation();
			if (codeLocation != null) {
		// Print summary when done
		publisher.registerHandlerFor(TestRunFinished.class, (event) -> {
			if (unusedSteps.isEmpty()) {

			out.println(unusedSteps.size() + " Unused steps:");

			// Output results when done
			for (Entry<String, String> entry : unusedSteps.entrySet()) {
				String location = entry.getKey();
				String pattern = entry.getValue();
				out.println(location + " # " + pattern);

It can be run easily through:

import cucumber.api.CucumberOptions;
import cucumber.api.junit.Cucumber;
import org.junit.runner.RunWith;

@CucumberOptions(dryRun = true, monochrome = true, plugin = "unused:target/unused.log")
public class RunCucumberTest {}

Notably I’ve added the dryRun argument here to skip feature execution, as it’s not needed to determine unused steps. Without it all cucumber features would be executed, which can take a considerable amount of time. Finally, our build script checks for any output and breaks if any is found:

./mvnw verify
if grep \# target/unused.json; then exit 1; fi