Skip to content

Commit

Permalink
[JUnit] Invoke @BeforeClass before TestRunStarted event
Browse files Browse the repository at this point in the history
JUnit is running Cucumber so the expectation is that JUnit's
`@BeforeClass` and `@AfterClass` methods are invoked respectively before
and after all events emitted by Cucumber. However because the
`TestRunStarted` event was emitted in the constructor of `Cucumber` it
would precede the the invocation of `@BeforeClass`.

In older versions of Cucumber sending the `TestSourceRead` events was
coupled to reading the test sources. This made it impossible to read the
features ahead of time -required to to create the test description
tree and fail on lexer errors- and send `TestRunStarted` after
`@BeforeClass`.

This is no longer case since #1367 and so this problem can be resolved.
  • Loading branch information
mpkorstanje committed Mar 8, 2019
1 parent 0dd53da commit 267e6a3
Show file tree
Hide file tree
Showing 3 changed files with 128 additions and 40 deletions.
6 changes: 6 additions & 0 deletions junit/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -26,5 +26,11 @@
<artifactId>mockito-core</artifactId>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-library</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
86 changes: 46 additions & 40 deletions junit/src/main/java/cucumber/api/junit/Cucumber.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,26 +5,26 @@
import cucumber.api.event.TestRunFinished;
import cucumber.api.event.TestRunStarted;
import cucumber.runner.EventBus;
import cucumber.runner.ThreadLocalRunnerSupplier;
import cucumber.runner.TimeService;
import cucumber.runner.TimeServiceEventBus;
import cucumber.runtime.BackendModuleBackendSupplier;
import cucumber.runtime.BackendSupplier;
import cucumber.runner.TimeServiceEventBus;
import cucumber.runtime.ClassFinder;
import cucumber.runtime.RuntimeOptions;
import cucumber.runtime.FeaturePathFeatureSupplier;
import cucumber.runtime.RuntimeOptions;
import cucumber.runtime.RuntimeOptionsFactory;
import cucumber.runtime.filter.Filters;
import cucumber.runtime.formatter.Plugins;
import cucumber.runtime.formatter.PluginFactory;
import cucumber.runtime.model.FeatureLoader;
import cucumber.runner.ThreadLocalRunnerSupplier;
import cucumber.runtime.RuntimeOptionsFactory;
import cucumber.runtime.formatter.Plugins;
import cucumber.runtime.io.MultiLoader;
import cucumber.runtime.io.ResourceLoader;
import cucumber.runtime.io.ResourceLoaderClassFinder;
import cucumber.runtime.junit.Assertions;
import cucumber.runtime.junit.FeatureRunner;
import cucumber.runtime.junit.JUnitOptions;
import cucumber.runtime.model.CucumberFeature;
import cucumber.runtime.model.FeatureLoader;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.ClassRule;
Expand All @@ -47,7 +47,7 @@
* &#64;CucumberOptions(plugin = "pretty")
* public class RunCukesTest {
* }
Fail * </pre></blockquote>
* </pre></blockquote>
* <p>
* By default Cucumber will look for {@code .feature} and glue files on the classpath, using the same resource
* path as the annotated class. For example, if the annotated class is {@code com.example.RunCucumber} then
Expand All @@ -63,11 +63,11 @@
* @see CucumberOptions
*/
public class Cucumber extends ParentRunner<FeatureRunner> {
private final List<FeatureRunner> children = new ArrayList<FeatureRunner>();
private final List<FeatureRunner> children = new ArrayList<>();
private final EventBus bus;
private final ThreadLocalRunnerSupplier runnerSupplier;
private final Filters filters;
private final JUnitOptions junitOptions;
private final List<CucumberFeature> features;
private final Plugins plugins;

/**
* Constructor called by JUnit.
Expand All @@ -77,35 +77,36 @@ public class Cucumber extends ParentRunner<FeatureRunner> {
*/
public Cucumber(Class clazz) throws InitializationError {
super(clazz);
ClassLoader classLoader = clazz.getClassLoader();
Assertions.assertNoCucumberAnnotatedMethods(clazz);

// Parse the options early to provide fast feedback about invalid options
RuntimeOptionsFactory runtimeOptionsFactory = new RuntimeOptionsFactory(clazz);
RuntimeOptions runtimeOptions = runtimeOptionsFactory.create();
JUnitOptions junitOptions = new JUnitOptions(runtimeOptions.isStrict(), runtimeOptions.getJunitOptions());

ClassLoader classLoader = clazz.getClassLoader();
ResourceLoader resourceLoader = new MultiLoader(classLoader);
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);

// Parse the features early. Don't proceed when there are lexer errors
FeatureLoader featureLoader = new FeatureLoader(resourceLoader);
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(featureLoader, runtimeOptions);
// Parse the features early. Don't proceed when there are lexer errors
final List<CucumberFeature> features = featureSupplier.get();
this.features = featureSupplier.get();

ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
BackendSupplier backendSupplier = new BackendModuleBackendSupplier(resourceLoader, classFinder, runtimeOptions);
// Create plugins after feature parsing to avoid the creation of empty files on lexer errors.
this.bus = new TimeServiceEventBus(TimeService.SYSTEM);
Plugins plugins = new Plugins(classLoader, new PluginFactory(), bus, runtimeOptions);
this.plugins = new Plugins(classLoader, new PluginFactory(), bus, runtimeOptions);


BackendSupplier backendSupplier = new BackendModuleBackendSupplier(resourceLoader, classFinder, runtimeOptions);
this.runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier);
this.filters = new Filters(runtimeOptions);
this.junitOptions = new JUnitOptions(runtimeOptions.isStrict(), runtimeOptions.getJunitOptions());
final StepDefinitionReporter stepDefinitionReporter = plugins.stepDefinitionReporter();

// Start the run before reading the features.
// Allows the test source read events to be broadcast properly
bus.send(new TestRunStarted(bus.getTime()));
for (CucumberFeature feature : features) {
feature.sendTestSourceRead(bus);
Filters filters = new Filters(runtimeOptions);
for (CucumberFeature cucumberFeature : features) {
FeatureRunner featureRunner = new FeatureRunner(cucumberFeature, filters, runnerSupplier, junitOptions);
if (!featureRunner.isEmpty()) {
children.add(featureRunner);
}
}
runnerSupplier.get().reportStepDefinitions(stepDefinitionReporter);
addChildren(features);
}

@Override
Expand All @@ -125,22 +126,27 @@ protected void runChild(FeatureRunner child, RunNotifier notifier) {

@Override
protected Statement childrenInvoker(RunNotifier notifier) {
final Statement features = super.childrenInvoker(notifier);
return new Statement() {
@Override
public void evaluate() throws Throwable {
features.evaluate();
bus.send(new TestRunFinished(bus.getTime()));
}
};
Statement runFeatures = super.childrenInvoker(notifier);
return new RunCucumber(runFeatures);
}

private void addChildren(List<CucumberFeature> cucumberFeatures) throws InitializationError {
for (CucumberFeature cucumberFeature : cucumberFeatures) {
FeatureRunner featureRunner = new FeatureRunner(cucumberFeature, filters, runnerSupplier, junitOptions);
if (!featureRunner.isEmpty()) {
children.add(featureRunner);
class RunCucumber extends Statement {
private final Statement runFeatures;

RunCucumber(Statement runFeatures) {
this.runFeatures = runFeatures;
}

@Override
public void evaluate() throws Throwable {
bus.send(new TestRunStarted(bus.getTime()));
for (CucumberFeature feature : features) {
feature.sendTestSourceRead(bus);
}
StepDefinitionReporter stepDefinitionReporter = plugins.stepDefinitionReporter();
runnerSupplier.get().reportStepDefinitions(stepDefinitionReporter);
runFeatures.evaluate();
bus.send(new TestRunFinished(bus.getTime()));
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package cucumber.runtime.junit;

import cucumber.api.CucumberOptions;
import cucumber.api.event.ConcurrentEventListener;
import cucumber.api.event.EventHandler;
import cucumber.api.event.EventPublisher;
import cucumber.api.event.TestRunFinished;
import cucumber.api.event.TestRunStarted;
import cucumber.api.junit.Cucumber;
import org.junit.AfterClass;
import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.model.InitializationError;

import java.util.ArrayList;
import java.util.List;

import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
import static org.junit.Assert.assertThat;

public class InvokeMethodsAroundEventsTest {

private static final List<String> events = new ArrayList<>();

private static EventHandler<TestRunStarted> testRunStartedEventHandler = new EventHandler<TestRunStarted>() {
@Override
public void receive(TestRunStarted event) {
events.add("TestRunStarted");
}
};
private static EventHandler<TestRunFinished> testRunFinishedEventHandler = new EventHandler<TestRunFinished>() {
@Override
public void receive(TestRunFinished event) {
events.add("TestRunFinished");
}
};

@AfterClass
public static void afterClass() {
events.clear();
}

@Test
public void finds_features_based_on_implicit_package() throws InitializationError {
Cucumber cucumber = new Cucumber(BeforeAfterClass.class);
cucumber.run(new RunNotifier());
assertThat(events, contains("BeforeClass", "TestRunStarted", "TestRunFinished", "AfterClass"));
}

@CucumberOptions(plugin = {"cucumber.runtime.junit.InvokeMethodsAroundEventsTest$TestRunStartedFinishedListener"})
public static class BeforeAfterClass {

@BeforeClass
public static void beforeClass() {
events.add("BeforeClass");

}

@AfterClass
public static void afterClass() {
events.add("AfterClass");
}
}

@SuppressWarnings("unused") // Used as a plugin by BeforeAfterClass
public static class TestRunStartedFinishedListener implements ConcurrentEventListener {

@Override
public void setEventPublisher(EventPublisher publisher) {
publisher.registerHandlerFor(TestRunStarted.class, testRunStartedEventHandler);
publisher.registerHandlerFor(TestRunFinished.class, testRunFinishedEventHandler);
}

}
}

0 comments on commit 267e6a3

Please sign in to comment.