From 267e6a3b63d4bc2588f99ea2c916fe1b191dfe1d Mon Sep 17 00:00:00 2001 From: mpkorstanje Date: Fri, 8 Mar 2019 11:09:25 +0100 Subject: [PATCH] [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. --- junit/pom.xml | 6 ++ .../java/cucumber/api/junit/Cucumber.java | 86 ++++++++++--------- .../junit/InvokeMethodsAroundEventsTest.java | 76 ++++++++++++++++ 3 files changed, 128 insertions(+), 40 deletions(-) create mode 100644 junit/src/test/java/cucumber/runtime/junit/InvokeMethodsAroundEventsTest.java diff --git a/junit/pom.xml b/junit/pom.xml index cfc29a7f37..90fbfd1b0d 100644 --- a/junit/pom.xml +++ b/junit/pom.xml @@ -26,5 +26,11 @@ mockito-core test + + + org.hamcrest + hamcrest-library + test + diff --git a/junit/src/main/java/cucumber/api/junit/Cucumber.java b/junit/src/main/java/cucumber/api/junit/Cucumber.java index baac3b8707..d2df5b122c 100644 --- a/junit/src/main/java/cucumber/api/junit/Cucumber.java +++ b/junit/src/main/java/cucumber/api/junit/Cucumber.java @@ -5,19 +5,18 @@ 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; @@ -25,6 +24,7 @@ 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; @@ -47,7 +47,7 @@ * @CucumberOptions(plugin = "pretty") * public class RunCukesTest { * } -Fail * + * *

* 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 @@ -63,11 +63,11 @@ * @see CucumberOptions */ public class Cucumber extends ParentRunner { - private final List children = new ArrayList(); + private final List children = new ArrayList<>(); private final EventBus bus; private final ThreadLocalRunnerSupplier runnerSupplier; - private final Filters filters; - private final JUnitOptions junitOptions; + private final List features; + private final Plugins plugins; /** * Constructor called by JUnit. @@ -77,35 +77,36 @@ public class Cucumber extends ParentRunner { */ 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 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 @@ -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 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())); } } } diff --git a/junit/src/test/java/cucumber/runtime/junit/InvokeMethodsAroundEventsTest.java b/junit/src/test/java/cucumber/runtime/junit/InvokeMethodsAroundEventsTest.java new file mode 100644 index 0000000000..fabe16fb82 --- /dev/null +++ b/junit/src/test/java/cucumber/runtime/junit/InvokeMethodsAroundEventsTest.java @@ -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 events = new ArrayList<>(); + + private static EventHandler testRunStartedEventHandler = new EventHandler() { + @Override + public void receive(TestRunStarted event) { + events.add("TestRunStarted"); + } + }; + private static EventHandler testRunFinishedEventHandler = new EventHandler() { + @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); + } + + } +}