Skip to content

Commit 0f2b83c

Browse files
authored
[Spring] Inject CucumberContextConfiguration constructor dependencies (#2664)
In #2661 classes annotated with `@CucumberContextConfiguration` were created as beans directly from the bean factory. This allowed them to be prepared as test instances by JUnit. However, we did not tell the factory that it should autowire constructor dependencies resulting in #2663. Additionally, because beans were created directly from the bean factory, they were not added to the application context. This meant that while dependencies could be injected into them, they could not be injected into other objects. Fixes: #2663
1 parent fe2d667 commit 0f2b83c

File tree

3 files changed

+150
-7
lines changed

3 files changed

+150
-7
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/)
1010
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
1111

1212
## [Unreleased]
13-
13+
### Fixed
14+
- [Spring] Inject CucumberContextConfiguration constructor dependencies ([#2664](https://github.com/cucumber/cucumber-jvm/pull/2664) M.P. Korstanje)
15+
1416
## [7.10.0] - 2022-12-11
1517
### Added
1618
- Enabled reproducible builds ([#2641](https://github.com/cucumber/cucumber-jvm/issues/2641) Hervé Boutemy )

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

Lines changed: 20 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@
1515
import java.util.Deque;
1616

1717
import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE;
18-
import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_NO;
18+
import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR;
1919

2020
class TestContextAdaptor {
2121

@@ -92,11 +92,25 @@ private void createAndPrepareTestInstance() {
9292
// using their default constructor and now allow them to be injected
9393
// into other step definition classes.
9494
try {
95-
Class<?> delegateTestClass = delegate.getTestContext().getTestClass();
96-
Object delegateTestInstance = applicationContext.getBeanFactory().autowire(delegateTestClass, AUTOWIRE_NO,
97-
false);
98-
delegate.prepareTestInstance(delegateTestInstance);
99-
this.delegateTestInstance = delegateTestInstance;
95+
Class<?> beanClass = delegate.getTestContext().getTestClass();
96+
97+
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
98+
// Note: By providing AUTOWIRE_CONSTRUCTOR the
99+
// AbstractAutowireCapableBeanFactory does not invoke
100+
// 'populateBean' and effectively creates a raw bean.
101+
Object bean = beanFactory.autowire(beanClass, AUTOWIRE_CONSTRUCTOR, false);
102+
103+
// But it works out well for us. Because now the
104+
// DependencyInjectionTestExecutionListener will invoke
105+
// 'autowireBeanProperties' which will populate the bean.
106+
delegate.prepareTestInstance(bean);
107+
108+
// Because the bean is created by a factory, it is not added to
109+
// the application context yet.
110+
CucumberTestContext scenarioScope = CucumberTestContext.getInstance();
111+
scenarioScope.put(beanClass.getName(), bean);
112+
113+
this.delegateTestInstance = bean;
100114
} catch (Exception e) {
101115
throw new CucumberBackendException(e.getMessage(), e);
102116
}

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

Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,30 @@
11
package io.cucumber.spring;
22

33
import io.cucumber.core.backend.CucumberBackendException;
4+
import io.cucumber.spring.beans.BellyBean;
5+
import io.cucumber.spring.beans.DummyComponent;
46
import org.junit.jupiter.api.AfterEach;
57
import org.junit.jupiter.api.Test;
68
import org.junit.jupiter.api.extension.ExtendWith;
9+
import org.junit.jupiter.params.ParameterizedTest;
10+
import org.junit.jupiter.params.provider.ValueSource;
711
import org.mockito.InOrder;
812
import org.mockito.Mock;
913
import org.mockito.Mockito;
1014
import org.mockito.junit.jupiter.MockitoExtension;
15+
import org.springframework.beans.factory.BeanNameAware;
16+
import org.springframework.beans.factory.annotation.Autowired;
17+
import org.springframework.lang.NonNull;
1118
import org.springframework.test.context.ContextConfiguration;
1219
import org.springframework.test.context.TestContextManager;
1320
import org.springframework.test.context.TestExecutionListener;
1421

1522
import static java.util.Collections.singletonList;
23+
import static org.junit.jupiter.api.Assertions.assertAll;
24+
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
25+
import static org.junit.jupiter.api.Assertions.assertEquals;
26+
import static org.junit.jupiter.api.Assertions.assertNotNull;
27+
import static org.junit.jupiter.api.Assertions.assertSame;
1628
import static org.junit.jupiter.api.Assertions.assertThrows;
1729
import static org.mockito.ArgumentMatchers.any;
1830
import static org.mockito.Mockito.doThrow;
@@ -182,10 +194,125 @@ void invokesAllMethodsPriorIfAfterTestClassThrows() throws Exception {
182194
inOrder.verify(listener).afterTestClass(any());
183195
}
184196

197+
@ParameterizedTest
198+
@ValueSource(classes = { WithAutowiredDependency.class, WithConstructorDependency.class })
199+
void autowireAndPostProcessesOnlyOnce(Class<? extends Spy> testClass) {
200+
TestContextManager manager = new TestContextManager(testClass);
201+
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(testClass));
202+
203+
assertAll(
204+
() -> assertDoesNotThrow(adaptor::start),
205+
() -> assertNotNull(manager.getTestContext().getTestInstance()),
206+
() -> assertSame(manager.getTestContext().getTestInstance(), adaptor.getInstance(testClass)),
207+
() -> assertEquals(1, adaptor.getInstance(testClass).autowiredCount()),
208+
() -> assertEquals(1, adaptor.getInstance(testClass).postProcessedCount()),
209+
() -> assertNotNull(adaptor.getInstance(testClass).getBelly()),
210+
() -> assertNotNull(adaptor.getInstance(testClass).getDummyComponent()),
211+
() -> assertDoesNotThrow(adaptor::stop));
212+
}
213+
185214
@CucumberContextConfiguration
186215
@ContextConfiguration("classpath:cucumber.xml")
187216
public static class SomeContextConfiguration {
188217

189218
}
190219

220+
private interface Spy {
221+
222+
int postProcessedCount();
223+
224+
int autowiredCount();
225+
226+
BellyBean getBelly();
227+
228+
DummyComponent getDummyComponent();
229+
230+
}
231+
232+
@CucumberContextConfiguration
233+
@ContextConfiguration("classpath:cucumber.xml")
234+
public static class WithAutowiredDependency implements BeanNameAware, Spy {
235+
236+
@Autowired
237+
BellyBean belly;
238+
239+
int postProcessedCount = 0;
240+
int autowiredCount = 0;
241+
242+
private DummyComponent dummyComponent;
243+
244+
@Autowired
245+
public void setDummyComponent(DummyComponent dummyComponent) {
246+
this.dummyComponent = dummyComponent;
247+
this.autowiredCount++;
248+
}
249+
250+
@Override
251+
public void setBeanName(@NonNull String ignored) {
252+
postProcessedCount++;
253+
}
254+
255+
@Override
256+
public int postProcessedCount() {
257+
return postProcessedCount;
258+
}
259+
260+
@Override
261+
public int autowiredCount() {
262+
return autowiredCount;
263+
}
264+
265+
@Override
266+
public BellyBean getBelly() {
267+
return belly;
268+
}
269+
270+
@Override
271+
public DummyComponent getDummyComponent() {
272+
return dummyComponent;
273+
}
274+
}
275+
276+
@CucumberContextConfiguration
277+
@ContextConfiguration("classpath:cucumber.xml")
278+
public static class WithConstructorDependency implements BeanNameAware, Spy {
279+
280+
final BellyBean belly;
281+
final DummyComponent dummyComponent;
282+
283+
int postProcessedCount = 0;
284+
int autowiredCount = 0;
285+
286+
public WithConstructorDependency(BellyBean belly, DummyComponent dummyComponent) {
287+
this.belly = belly;
288+
this.dummyComponent = dummyComponent;
289+
this.autowiredCount++;
290+
}
291+
292+
@Override
293+
public void setBeanName(@NonNull String ignored) {
294+
postProcessedCount++;
295+
}
296+
297+
@Override
298+
public int postProcessedCount() {
299+
return postProcessedCount;
300+
}
301+
302+
@Override
303+
public int autowiredCount() {
304+
return autowiredCount;
305+
}
306+
307+
@Override
308+
public BellyBean getBelly() {
309+
return belly;
310+
}
311+
312+
@Override
313+
public DummyComponent getDummyComponent() {
314+
return dummyComponent;
315+
}
316+
}
317+
191318
}

0 commit comments

Comments
 (0)