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 created the following plugin.

import cucumber.api.StepDefinitionReporter;
import cucumber.api.SummaryPrinter;
import cucumber.api.event.EventListener;
import cucumber.api.event.EventPublisher;
import cucumber.api.event.TestRunFinished;
import cucumber.api.event.TestStepFinished;
import cucumber.api.formatter.NiceAppendable;
import cucumber.runtime.StepDefinition;
import gherkin.deps.com.google.gson.Gson;
import gherkin.deps.com.google.gson.GsonBuilder;
import java.util.*;

public class UnusedStepsPlugin implements EventListener, SummaryPrinter, StepDefinitionReporter {

    private final Map unusedSteps = new TreeMap<>();
    private final Set usedSteps = new TreeSet<>();

    private final NiceAppendable out;

    public UnusedStepsPlugin(Appendable out) {
        this.out = new NiceAppendable(out);
    }

    @Override
    public void stepDefinition(StepDefinition stepDefinition) {
        // Record all steps available
        unusedSteps.put(stepDefinition.getLocation(false), stepDefinition.getPattern());
    }

    @Override
    public void setEventPublisher(EventPublisher publisher) {
        // Record any steps that run
        publisher.registerHandlerFor(TestStepFinished.class,
                event -> Optional.ofNullable(event.testStep.getCodeLocation()).ifPresent(usedSteps::add));
        // Print summary when done
        publisher.registerHandlerFor(TestRunFinished.class, event -> printSummary());
    }

    public void printSummary() {
        // Subtract any steps that ran
        usedSteps.forEach(unusedSteps::remove);
        System.out.printf("%d Unused steps", unusedSteps.size());

        // Record results when done
        out.append(gson().toJson(unusedSteps));
        out.close();
    }

    private static Gson gson() {
        return new GsonBuilder().setPrettyPrinting().create();
    }
}

It can be run easily through:

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

@RunWith(Cucumber.class)
@CucumberOptions(plugin = { "com.example.UnusedStepsPlugin:target/unused.json" }, dryRun = true)
public class UnusedStepsTest {}

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 -i [a-z] target/unused.json; then exit 1; fi

Update: The above plugin only reports on annotation based Cucumber steps; Java8 lambda based steps are now supported in 4.4+ following a contribution here: https://github.com/cucumber/cucumber-jvm/pull/1634

import java.util.Map;
import java.util.Optional;
import java.util.TreeMap;

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

public class UnusedStepsPlugin implements EventListener, SummaryPrinter {

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

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

	@Override
	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 -> Optional.ofNullable(event.testStep.getCodeLocation()).ifPresent(unusedSteps::remove));
		// Print summary when done
		publisher.registerHandlerFor(TestRunFinished.class, event -> printSummary());
	}

	private void printSummary() {
		// Output results when done
		out.append("" + unusedSteps.size()).println(" Unused steps:");
		unusedSteps.forEach((location, pattern) -> out.append(location).append(": ").println(pattern));
	}
}
shadow-left