Skip to content

Commit 267e6a3

Browse files
committed
[JUnit] Invoke @BeforeClass before TestRunStarted event
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.
1 parent 0dd53da commit 267e6a3

File tree

3 files changed

+128
-40
lines changed

3 files changed

+128
-40
lines changed

junit/pom.xml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,11 @@
2626
<artifactId>mockito-core</artifactId>
2727
<scope>test</scope>
2828
</dependency>
29+
30+
<dependency>
31+
<groupId>org.hamcrest</groupId>
32+
<artifactId>hamcrest-library</artifactId>
33+
<scope>test</scope>
34+
</dependency>
2935
</dependencies>
3036
</project>

junit/src/main/java/cucumber/api/junit/Cucumber.java

Lines changed: 46 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -5,26 +5,26 @@
55
import cucumber.api.event.TestRunFinished;
66
import cucumber.api.event.TestRunStarted;
77
import cucumber.runner.EventBus;
8+
import cucumber.runner.ThreadLocalRunnerSupplier;
89
import cucumber.runner.TimeService;
10+
import cucumber.runner.TimeServiceEventBus;
911
import cucumber.runtime.BackendModuleBackendSupplier;
1012
import cucumber.runtime.BackendSupplier;
11-
import cucumber.runner.TimeServiceEventBus;
1213
import cucumber.runtime.ClassFinder;
13-
import cucumber.runtime.RuntimeOptions;
1414
import cucumber.runtime.FeaturePathFeatureSupplier;
15+
import cucumber.runtime.RuntimeOptions;
16+
import cucumber.runtime.RuntimeOptionsFactory;
1517
import cucumber.runtime.filter.Filters;
16-
import cucumber.runtime.formatter.Plugins;
1718
import cucumber.runtime.formatter.PluginFactory;
18-
import cucumber.runtime.model.FeatureLoader;
19-
import cucumber.runner.ThreadLocalRunnerSupplier;
20-
import cucumber.runtime.RuntimeOptionsFactory;
19+
import cucumber.runtime.formatter.Plugins;
2120
import cucumber.runtime.io.MultiLoader;
2221
import cucumber.runtime.io.ResourceLoader;
2322
import cucumber.runtime.io.ResourceLoaderClassFinder;
2423
import cucumber.runtime.junit.Assertions;
2524
import cucumber.runtime.junit.FeatureRunner;
2625
import cucumber.runtime.junit.JUnitOptions;
2726
import cucumber.runtime.model.CucumberFeature;
27+
import cucumber.runtime.model.FeatureLoader;
2828
import org.junit.AfterClass;
2929
import org.junit.BeforeClass;
3030
import org.junit.ClassRule;
@@ -47,7 +47,7 @@
4747
* &#64;CucumberOptions(plugin = "pretty")
4848
* public class RunCukesTest {
4949
* }
50-
Fail * </pre></blockquote>
50+
* </pre></blockquote>
5151
* <p>
5252
* By default Cucumber will look for {@code .feature} and glue files on the classpath, using the same resource
5353
* path as the annotated class. For example, if the annotated class is {@code com.example.RunCucumber} then
@@ -63,11 +63,11 @@
6363
* @see CucumberOptions
6464
*/
6565
public class Cucumber extends ParentRunner<FeatureRunner> {
66-
private final List<FeatureRunner> children = new ArrayList<FeatureRunner>();
66+
private final List<FeatureRunner> children = new ArrayList<>();
6767
private final EventBus bus;
6868
private final ThreadLocalRunnerSupplier runnerSupplier;
69-
private final Filters filters;
70-
private final JUnitOptions junitOptions;
69+
private final List<CucumberFeature> features;
70+
private final Plugins plugins;
7171

7272
/**
7373
* Constructor called by JUnit.
@@ -77,35 +77,36 @@ public class Cucumber extends ParentRunner<FeatureRunner> {
7777
*/
7878
public Cucumber(Class clazz) throws InitializationError {
7979
super(clazz);
80-
ClassLoader classLoader = clazz.getClassLoader();
8180
Assertions.assertNoCucumberAnnotatedMethods(clazz);
8281

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

87+
ClassLoader classLoader = clazz.getClassLoader();
8688
ResourceLoader resourceLoader = new MultiLoader(classLoader);
89+
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
90+
91+
// Parse the features early. Don't proceed when there are lexer errors
8792
FeatureLoader featureLoader = new FeatureLoader(resourceLoader);
8893
FeaturePathFeatureSupplier featureSupplier = new FeaturePathFeatureSupplier(featureLoader, runtimeOptions);
89-
// Parse the features early. Don't proceed when there are lexer errors
90-
final List<CucumberFeature> features = featureSupplier.get();
94+
this.features = featureSupplier.get();
9195

92-
ClassFinder classFinder = new ResourceLoaderClassFinder(resourceLoader, classLoader);
93-
BackendSupplier backendSupplier = new BackendModuleBackendSupplier(resourceLoader, classFinder, runtimeOptions);
96+
// Create plugins after feature parsing to avoid the creation of empty files on lexer errors.
9497
this.bus = new TimeServiceEventBus(TimeService.SYSTEM);
95-
Plugins plugins = new Plugins(classLoader, new PluginFactory(), bus, runtimeOptions);
98+
this.plugins = new Plugins(classLoader, new PluginFactory(), bus, runtimeOptions);
99+
100+
101+
BackendSupplier backendSupplier = new BackendModuleBackendSupplier(resourceLoader, classFinder, runtimeOptions);
96102
this.runnerSupplier = new ThreadLocalRunnerSupplier(runtimeOptions, bus, backendSupplier);
97-
this.filters = new Filters(runtimeOptions);
98-
this.junitOptions = new JUnitOptions(runtimeOptions.isStrict(), runtimeOptions.getJunitOptions());
99-
final StepDefinitionReporter stepDefinitionReporter = plugins.stepDefinitionReporter();
100-
101-
// Start the run before reading the features.
102-
// Allows the test source read events to be broadcast properly
103-
bus.send(new TestRunStarted(bus.getTime()));
104-
for (CucumberFeature feature : features) {
105-
feature.sendTestSourceRead(bus);
103+
Filters filters = new Filters(runtimeOptions);
104+
for (CucumberFeature cucumberFeature : features) {
105+
FeatureRunner featureRunner = new FeatureRunner(cucumberFeature, filters, runnerSupplier, junitOptions);
106+
if (!featureRunner.isEmpty()) {
107+
children.add(featureRunner);
108+
}
106109
}
107-
runnerSupplier.get().reportStepDefinitions(stepDefinitionReporter);
108-
addChildren(features);
109110
}
110111

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

126127
@Override
127128
protected Statement childrenInvoker(RunNotifier notifier) {
128-
final Statement features = super.childrenInvoker(notifier);
129-
return new Statement() {
130-
@Override
131-
public void evaluate() throws Throwable {
132-
features.evaluate();
133-
bus.send(new TestRunFinished(bus.getTime()));
134-
}
135-
};
129+
Statement runFeatures = super.childrenInvoker(notifier);
130+
return new RunCucumber(runFeatures);
136131
}
137132

138-
private void addChildren(List<CucumberFeature> cucumberFeatures) throws InitializationError {
139-
for (CucumberFeature cucumberFeature : cucumberFeatures) {
140-
FeatureRunner featureRunner = new FeatureRunner(cucumberFeature, filters, runnerSupplier, junitOptions);
141-
if (!featureRunner.isEmpty()) {
142-
children.add(featureRunner);
133+
class RunCucumber extends Statement {
134+
private final Statement runFeatures;
135+
136+
RunCucumber(Statement runFeatures) {
137+
this.runFeatures = runFeatures;
138+
}
139+
140+
@Override
141+
public void evaluate() throws Throwable {
142+
bus.send(new TestRunStarted(bus.getTime()));
143+
for (CucumberFeature feature : features) {
144+
feature.sendTestSourceRead(bus);
143145
}
146+
StepDefinitionReporter stepDefinitionReporter = plugins.stepDefinitionReporter();
147+
runnerSupplier.get().reportStepDefinitions(stepDefinitionReporter);
148+
runFeatures.evaluate();
149+
bus.send(new TestRunFinished(bus.getTime()));
144150
}
145151
}
146152
}
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
package cucumber.runtime.junit;
2+
3+
import cucumber.api.CucumberOptions;
4+
import cucumber.api.event.ConcurrentEventListener;
5+
import cucumber.api.event.EventHandler;
6+
import cucumber.api.event.EventPublisher;
7+
import cucumber.api.event.TestRunFinished;
8+
import cucumber.api.event.TestRunStarted;
9+
import cucumber.api.junit.Cucumber;
10+
import org.junit.AfterClass;
11+
import org.junit.BeforeClass;
12+
import org.junit.Test;
13+
import org.junit.runner.notification.RunNotifier;
14+
import org.junit.runners.model.InitializationError;
15+
16+
import java.util.ArrayList;
17+
import java.util.List;
18+
19+
import static org.hamcrest.collection.IsIterableContainingInOrder.contains;
20+
import static org.junit.Assert.assertThat;
21+
22+
public class InvokeMethodsAroundEventsTest {
23+
24+
private static final List<String> events = new ArrayList<>();
25+
26+
private static EventHandler<TestRunStarted> testRunStartedEventHandler = new EventHandler<TestRunStarted>() {
27+
@Override
28+
public void receive(TestRunStarted event) {
29+
events.add("TestRunStarted");
30+
}
31+
};
32+
private static EventHandler<TestRunFinished> testRunFinishedEventHandler = new EventHandler<TestRunFinished>() {
33+
@Override
34+
public void receive(TestRunFinished event) {
35+
events.add("TestRunFinished");
36+
}
37+
};
38+
39+
@AfterClass
40+
public static void afterClass() {
41+
events.clear();
42+
}
43+
44+
@Test
45+
public void finds_features_based_on_implicit_package() throws InitializationError {
46+
Cucumber cucumber = new Cucumber(BeforeAfterClass.class);
47+
cucumber.run(new RunNotifier());
48+
assertThat(events, contains("BeforeClass", "TestRunStarted", "TestRunFinished", "AfterClass"));
49+
}
50+
51+
@CucumberOptions(plugin = {"cucumber.runtime.junit.InvokeMethodsAroundEventsTest$TestRunStartedFinishedListener"})
52+
public static class BeforeAfterClass {
53+
54+
@BeforeClass
55+
public static void beforeClass() {
56+
events.add("BeforeClass");
57+
58+
}
59+
60+
@AfterClass
61+
public static void afterClass() {
62+
events.add("AfterClass");
63+
}
64+
}
65+
66+
@SuppressWarnings("unused") // Used as a plugin by BeforeAfterClass
67+
public static class TestRunStartedFinishedListener implements ConcurrentEventListener {
68+
69+
@Override
70+
public void setEventPublisher(EventPublisher publisher) {
71+
publisher.registerHandlerFor(TestRunStarted.class, testRunStartedEventHandler);
72+
publisher.registerHandlerFor(TestRunFinished.class, testRunFinishedEventHandler);
73+
}
74+
75+
}
76+
}

0 commit comments

Comments
 (0)