Deconstruction of the Cucumber JUnit Runner Behavior

When done well and effectively, BDD or Behavior-Driven Development can set a company apart from its competitors by creating a collaborative channel between its technical and business teams and constructing a common platform to discuss/document a shared understanding of the problems to be solved by them. To facilitate this collaboration and to shorten the product feedback loops in the small and rapid product iterations, tools like Cucumber enter into the picture.

In this blog post, I will not get into the details of Cucumber and its different features. I have covered those in another blog post – You can check it out here if you haven’t already. To put it briefly, it is a collaborative tool that allows teams to write executable specifications in plain text and helps them to validate whether the product they are building conforms to what the specifications say. To understand and work with the plain text for writing specifications, Cucumber uses a set of grammar rules (Gherkin).

What is Cucumber JUnit Runner?
Let’s first understand what a “JUnit Runner” is and how it works internally.

A “JUnit Runner” is any class that extends the main “Runner” class of JUnit4 – an abstract JUnit class that is responsible for running our JUnit tests (from our JUnit test classes) and for notifying the JUnit “RunNotifier” class of significant events fired during the test run.

The “Cucumber JUnit Runner (Cucumber)” is one of such custom extension subclasses of the “Runner” class and is created to modify the default JUnit test execution process for Cucumber. It notifies JUnit of its progress while running the executable specifications (once per run). This “Cucumber JUnit Runner (Cucumber)” which is used to run these executable specifications can be set using the JUnit “@RunWith” annotation in any class.

package com.cucumber.testRunner;

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

@RunWith(Cucumber.class)
public class TestRunner {
}

The above-mentioned “Cucumber” class is a subclass of the JUnit “ParentRunner” class which is a specialized subclass of the JUnit main “Runner” class. The abstract “ParentRunner” class provides most of the functionality specific to the custom “Cucumber JUnit Runner (Cucumber)” like handling BeforeClass, AfterClass annotations and runs the specifications in a hierarchical manner (like a tree). Think of the hierarchical like running a test suite that contains other test suites, which might contain multiple test classes and each of these test classes can contain multiple test methods.

How does the Cucumber JUnit Runner execution start?
The execution gets started when the “runClasses(Class<?>… classes)” method the “JUnitCore” class of JUnit is invoked using the command line (by passing the test classes) or from IDE. This “JUnitCore” class then uses reflection to find the appropriate runner for the passed test classes by searching for the “@RunWith” annotation in them. If no runner is found, then it uses the default JUnit runner (BlockJUnit4ClassRunner). In this way, the runner gets instantiated with the passed test classes and then the runner instantiates and runs those test classes.

How to create a custom Cucumber JUnit Runner wrapper?
There might be occasions where you would want to create your customizable Test Runner instead of using the default Cucumber JUnit Test Runner.  To do this, you have to extend the “Cucumber” class in your custom runner class. Let’s give the custom class a name – “ExtendedCucumber”.

package com.cucumber.testRunner;

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

@RunWith(ExtendedCucumber.class)
@CucumberOptions(features = "FeatureFiles", glue = {
		"com.cucumber" }, monochrome = true, dryRun = false, strict = false, plugin = {
		"pretty:target/cucumber-pretty-report", "html:target/cucumber-html-report",
		"junit:target/cucumber-junit-report/cucumber-results.xml",
		"json:target/cucumber-json-report/cucumber.json",
		"usage:target/cucumber-usage-report/cucumber-usage.json" }, tags = { "@Scenario1" })

public class TestRunner {

}

Creating and using a custom runner will help you add things like adding custom listeners which will help you to take different actions or display descriptions on different events of the test execution.

In the below example, consider the CustomListener class which extends the JUnit4 RunListener class:

package com.cucumber.reportGeneration;

import org.junit.runner.Description;
import org.junit.runner.Result;
import org.junit.runner.notification.Failure;
import org.junit.runner.notification.RunListener;

public class CustomListener extends RunListener {

	@Override
	public void testAssumptionFailure(Failure failure) {
		System.out.println("testAssumptionFailure");
	}

	@Override
	public void testFailure(Failure failure) throws Exception {
		System.out.println("testFailure");
	}

	@Override
	public void testFinished(Description description) throws Exception {
		System.out.println("testFinished");
	}

	@Override
	public void testIgnored(Description description) throws Exception {
		System.out.println("testIgnored");
	}

	@Override
	public void testRunFinished(Result result) throws Exception {
		System.out.println("testRunFinished");
	}

	@Override
	public void testRunStarted(Description description) throws Exception {
		System.out.println("testRunStarted");
	}

	@Override
	public void testStarted(Description description) throws Exception {
		System.out.println("testStarted");
	}

}

… and the custom Cucumber JUnit Runner class (ExtendedCucumber) is invoking its methods during the run.

package com.cucumber.testRunner;

import java.io.IOException;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;
import com.cucumber.reportGeneration.CustomListener;
import cucumber.api.junit.Cucumber;

public class ExtendedCucumber extends Cucumber {

	public ExtendedCucumber(Class clazz) throws InitializationError, IOException {
		super(clazz);
	}

	@Override
	public void run(RunNotifier notifier) {
		notifier.addListener(new CustomListener());
		super.run(notifier);
	}

}

We can also customize/extract information(as per our need) of different Cucumber behaviors/features like a rerun, reporting, tags, scenarios, feature elements, scenario outline, etc. or we can set various system properties using this “ExtendedCucumber” custom runner class at runtime.