Skip to content

Commit df2042b

Browse files
authored
[Spring] Invoke all TestContextManager methods (#2661)
To make writing tests with Spring easier Spring provides a `TestContextManager`. This classes provides call backs for various `TestExecutionListeners`. These are then used by various extensions such as the `MockitoTestExecutionListener` which injects `@MockBeans` into test instances. When all methods are not invoked this leads to problems such as (#2654,#2655,#2656) While this was initially (#1470) not a problem, it appears that various listener implementations have started to assume that all methods would be invoked. Closes: #2655 Fixes: #2654, #2572
1 parent 7527153 commit df2042b

File tree

4 files changed

+312
-67
lines changed

4 files changed

+312
-67
lines changed

CHANGELOG.md

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
1212
## [Unreleased]
1313

1414
### Added
15-
- Enabled reproducible builds ([2641](https://github.com/cucumber/cucumber-jvm/issues/2641) Hervé Boutemy )
16-
- [Core] Mark Allure 5 and 6 plugins as incompatible ([2652](https://github.com/cucumber/cucumber-jvm/issues/2652) M.P. Korstanje)
15+
- Enabled reproducible builds ([#2641](https://github.com/cucumber/cucumber-jvm/issues/2641) Hervé Boutemy )
16+
- [Core] Mark Allure 5 and 6 plugins as incompatible ([#2652](https://github.com/cucumber/cucumber-jvm/issues/2652) M.P. Korstanje)
17+
- [Spring] Invoke all `TestContextManager` methods ([#2661](https://github.com/cucumber/cucumber-jvm/pull/2661) M.P. Korstanje)
1718

1819
## Fixed
19-
- [Core] Emit exceptions on failure to handle test run finished events ([2651](https://github.com/cucumber/cucumber-jvm/issues/2651) M.P. Korstanje)
20+
- [Core] Emit exceptions on failure to handle test run finished events ([#2651](https://github.com/cucumber/cucumber-jvm/issues/2651) M.P. Korstanje)
21+
- [Spring] @MockBean annotation not working with JUnit5 ([#2654](https://github.com/cucumber/cucumber-jvm/pull/2654) Alexander Kirilov, M.P. Korstanje)
2022

2123
## Deprecated
2224
- [DeltaSpike] Deprecated Deltaspike - does not work on Java 17.
@@ -26,14 +28,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
2628
- [Core] Update dependency io.cucumber:gherkin to v25.0.2. Japanese Rule translation changed from Rule to ルール.
2729

2830
### Added
29-
- [Spring] Support @CucumberContextConfiguration as a meta-annotation ([2491](https://github.com/cucumber/cucumber-jvm/issues/2491) Michael Schlatt)
31+
- [Spring] Support @CucumberContextConfiguration as a meta-annotation ([#2491](https://github.com/cucumber/cucumber-jvm/issues/2491) Michael Schlatt)
3032

3133
### Changed
3234
- [Core] Update dependency io.cucumber:gherkin to v24.1
33-
- [Core] Delegate encoding and BOM handling to gherkin ([2624](https://github.com/cucumber/cucumber-jvm/issues/2624) M.P. Korstanje)
35+
- [Core] Delegate encoding and BOM handling to gherkin ([#2624](https://github.com/cucumber/cucumber-jvm/issues/2624) M.P. Korstanje)
3436

3537
### Fixed
36-
- [Core] Don't swallow parse errors on the CLI ([2632](https://github.com/cucumber/cucumber-jvm/issues/2632) M.P. Korstanje)
38+
- [Core] Don't swallow parse errors on the CLI ([#2632](https://github.com/cucumber/cucumber-jvm/issues/2632) M.P. Korstanje)
3739

3840
### Security
3941
- [Core] Update dependency com.fasterxml.jackson to v2.13.4.20221012

cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java

Lines changed: 108 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,12 @@
1010
import org.springframework.test.context.TestContextManager;
1111

1212
import java.lang.reflect.Method;
13+
import java.util.ArrayDeque;
1314
import java.util.Collection;
15+
import java.util.Deque;
1416

1517
import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE;
18+
import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_NO;
1619

1720
class TestContextAdaptor {
1821

@@ -21,6 +24,7 @@ class TestContextAdaptor {
2124
private final TestContextManager delegate;
2225
private final ConfigurableApplicationContext applicationContext;
2326
private final Collection<Class<?>> glueClasses;
27+
private final Deque<Runnable> stopInvocations = new ArrayDeque<>();
2428
private Object delegateTestInstance;
2529

2630
TestContextAdaptor(
@@ -44,23 +48,70 @@ public final void start() {
4448
registerGlueCodeScope(applicationContext);
4549
registerStepClassBeanDefinitions(applicationContext.getBeanFactory());
4650
}
51+
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestClass);
4752
notifyContextManagerAboutBeforeTestClass();
48-
CucumberTestContext.getInstance().start();
53+
stopInvocations.push(this::stopCucumberTestContext);
54+
startCucumberTestContext();
55+
stopInvocations.push(this::disposeTestInstance);
56+
createAndPrepareTestInstance();
57+
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestMethod);
4958
notifyTestContextManagerAboutBeforeTestMethod();
59+
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestExecution);
60+
notifyTestContextManagerAboutBeforeExecution();
5061
}
5162

52-
private void notifyTestContextManagerAboutBeforeTestMethod() {
63+
private void notifyContextManagerAboutBeforeTestClass() {
64+
try {
65+
delegate.beforeTestClass();
66+
} catch (Exception e) {
67+
throw new CucumberBackendException(e.getMessage(), e);
68+
}
69+
}
70+
71+
private void startCucumberTestContext() {
72+
CucumberTestContext.getInstance().start();
73+
}
74+
75+
private void createAndPrepareTestInstance() {
76+
// Unlike JUnit, Cucumber does not have a single test class.
77+
// Springs TestContext however assumes we do, and we are expected to
78+
// create an instance of it using the default constructor.
79+
//
80+
// Users of Cucumber would however like to inject their step
81+
// definition classes into other step definition classes. This requires
82+
// that the test instance exists in the application context as a bean.
83+
//
84+
// Normally when a bean is pulled from the application context with
85+
// getBean it is also autowired. This will however conflict with
86+
// Springs DependencyInjectionTestExecutionListener. So we create
87+
// a raw bean here.
88+
//
89+
// This probably free from side effects, but at some point in the
90+
// future we may have to accept that the only way forward is to
91+
// construct instances annotated with @CucumberContextConfiguration
92+
// using their default constructor and now allow them to be injected
93+
// into other step definition classes.
5394
try {
5495
Class<?> delegateTestClass = delegate.getTestContext().getTestClass();
55-
delegateTestInstance = applicationContext.getBean(delegateTestClass);
56-
Method dummyMethod = TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
96+
Object delegateTestInstance = applicationContext.getBeanFactory().autowire(delegateTestClass, AUTOWIRE_NO,
97+
false);
98+
delegate.prepareTestInstance(delegateTestInstance);
99+
this.delegateTestInstance = delegateTestInstance;
100+
} catch (Exception e) {
101+
throw new CucumberBackendException(e.getMessage(), e);
102+
}
103+
}
104+
105+
private void notifyTestContextManagerAboutBeforeTestMethod() {
106+
try {
107+
Method dummyMethod = getDummyMethod();
57108
delegate.beforeTestMethod(delegateTestInstance, dummyMethod);
58109
} catch (Exception e) {
59110
throw new CucumberBackendException(e.getMessage(), e);
60111
}
61112
}
62113

63-
final void registerGlueCodeScope(ConfigurableApplicationContext context) {
114+
private void registerGlueCodeScope(ConfigurableApplicationContext context) {
64115
while (context != null) {
65116
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
66117
// Scenario scope may have already been registered by another
@@ -73,15 +124,15 @@ final void registerGlueCodeScope(ConfigurableApplicationContext context) {
73124
}
74125
}
75126

76-
private void notifyContextManagerAboutBeforeTestClass() {
127+
private void notifyTestContextManagerAboutBeforeExecution() {
77128
try {
78-
delegate.beforeTestClass();
129+
delegate.beforeTestExecution(delegateTestInstance, getDummyMethod());
79130
} catch (Exception e) {
80131
throw new CucumberBackendException(e.getMessage(), e);
81132
}
82133
}
83134

84-
final void registerStepClassBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
135+
private void registerStepClassBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
85136
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
86137
for (Class<?> glue : glueClasses) {
87138
registerStepClassBeanDefinition(registry, glue);
@@ -102,18 +153,23 @@ private void registerStepClassBeanDefinition(BeanDefinitionRegistry registry, Cl
102153
}
103154

104155
public final void stop() {
105-
// Don't invoke after test method when before test class was not invoked
106-
// this is implicit in the existence of an active the test context
107-
// session. This is not ideal, but Cucumber only supports 1 set of
108-
// before/after semantics while JUnit and Spring have 2 sets.
109-
if (CucumberTestContext.getInstance().isActive()) {
110-
if (delegateTestInstance != null) {
111-
notifyTestContextManagerAboutAfterTestMethod();
112-
delegateTestInstance = null;
156+
// Cucumber only supports 1 set of before/after semantics while JUnit
157+
// and Spring have 2 sets. So here we use a stack to ensure we don't
158+
// invoke only the matching after methods for each before methods.
159+
CucumberBackendException lastException = null;
160+
for (Runnable stopInvocation : stopInvocations) {
161+
try {
162+
stopInvocation.run();
163+
} catch (CucumberBackendException e) {
164+
if (lastException != null) {
165+
e.addSuppressed(lastException);
166+
}
167+
lastException = e;
113168
}
114-
CucumberTestContext.getInstance().stop();
115169
}
116-
notifyTestContextManagerAboutAfterTestClass();
170+
if (lastException != null) {
171+
throw lastException;
172+
}
117173
}
118174

119175
private void notifyTestContextManagerAboutAfterTestClass() {
@@ -124,11 +180,35 @@ private void notifyTestContextManagerAboutAfterTestClass() {
124180
}
125181
}
126182

183+
private void stopCucumberTestContext() {
184+
CucumberTestContext.getInstance().stop();
185+
}
186+
187+
private void disposeTestInstance() {
188+
delegateTestInstance = null;
189+
}
190+
127191
private void notifyTestContextManagerAboutAfterTestMethod() {
128192
try {
129193
Object delegateTestInstance = delegate.getTestContext().getTestInstance();
130-
Method dummyMethod = TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
131-
delegate.afterTestMethod(delegateTestInstance, dummyMethod, null);
194+
// Cucumber tests can throw exceptions, but we can't currently
195+
// get at them. So we provide null intentionally.
196+
// Cucumber also doesn't a single test method, so we provide a
197+
// dummy instead.
198+
delegate.afterTestMethod(delegateTestInstance, getDummyMethod(), null);
199+
} catch (Exception e) {
200+
throw new CucumberBackendException(e.getMessage(), e);
201+
}
202+
}
203+
204+
private void notifyTestContextManagerAboutAfterTestExecution() {
205+
try {
206+
Object delegateTestInstance = delegate.getTestContext().getTestInstance();
207+
// Cucumber tests can throw exceptions, but we can't currently
208+
// get at them. So we provide null intentionally.
209+
// Cucumber also doesn't a single test method, so we provide a
210+
// dummy instead.
211+
delegate.afterTestExecution(delegateTestInstance, getDummyMethod(), null);
132212
} catch (Exception e) {
133213
throw new CucumberBackendException(e.getMessage(), e);
134214
}
@@ -138,6 +218,14 @@ final <T> T getInstance(Class<T> type) {
138218
return applicationContext.getBean(type);
139219
}
140220

221+
private Method getDummyMethod() {
222+
try {
223+
return TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
224+
} catch (NoSuchMethodException e) {
225+
throw new RuntimeException(e);
226+
}
227+
}
228+
141229
public void cucumberDoesNotHaveASingleTestMethod() {
142230

143231
}

cucumber-spring/src/test/java/io/cucumber/spring/SpringFactoryTest.java

Lines changed: 5 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -347,15 +347,10 @@ void shouldBeStoppableWhenFacedWithMissingContextConfiguration() {
347347
assertDoesNotThrow(factory::stop);
348348
}
349349

350-
@ParameterizedTest
351-
@ValueSource(classes = {
352-
FailedBeforeTestClassContextConfiguration.class,
353-
FailedBeforeTestMethodContextConfiguration.class,
354-
FailedTestInstanceContextConfiguration.class
355-
})
356-
void shouldBeStoppableWhenFacedWithFailedApplicationContext(Class<?> contextConfiguration) {
350+
@Test
351+
void shouldBeStoppableWhenFacedWithFailedApplicationContext() {
357352
final ObjectFactory factory = new SpringFactory();
358-
factory.addClass(contextConfiguration);
353+
factory.addClass(FailedTestInstanceCreation.class);
359354

360355
assertThrows(CucumberBackendException.class, factory::start);
361356
assertDoesNotThrow(factory::stop);
@@ -414,40 +409,9 @@ public static class WithoutContextConfiguration {
414409

415410
@CucumberContextConfiguration
416411
@ContextConfiguration("classpath:cucumber.xml")
417-
@TestExecutionListeners(FailedBeforeTestClassContextConfiguration.FailingListener.class)
418-
public static class FailedBeforeTestClassContextConfiguration {
419-
420-
public static class FailingListener implements TestExecutionListener {
421-
422-
@Override
423-
public void beforeTestClass(TestContext testContext) throws Exception {
424-
throw new StubException();
425-
}
426-
427-
}
428-
429-
}
430-
431-
@CucumberContextConfiguration
432-
@ContextConfiguration("classpath:cucumber.xml")
433-
@TestExecutionListeners(FailedBeforeTestMethodContextConfiguration.FailingListener.class)
434-
public static class FailedBeforeTestMethodContextConfiguration {
435-
436-
public static class FailingListener implements TestExecutionListener {
437-
438-
@Override
439-
public void beforeTestMethod(TestContext testContext) throws Exception {
440-
throw new StubException();
441-
}
442-
443-
}
444-
445-
}
446-
@CucumberContextConfiguration
447-
@ContextConfiguration("classpath:cucumber.xml")
448-
public static class FailedTestInstanceContextConfiguration {
412+
public static class FailedTestInstanceCreation {
449413

450-
public FailedTestInstanceContextConfiguration() {
414+
public FailedTestInstanceCreation() {
451415
throw new RuntimeException();
452416
}
453417
}

0 commit comments

Comments
 (0)