From 0f2b83c0079f54e2c358a4374ba31f4e2838559e Mon Sep 17 00:00:00 2001 From: "M.P. Korstanje" Date: Fri, 16 Dec 2022 14:44:10 +0100 Subject: [PATCH] [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 --- CHANGELOG.md | 4 +- .../cucumber/spring/TestContextAdaptor.java | 26 +++- .../spring/TestTestContextAdaptorTest.java | 127 ++++++++++++++++++ 3 files changed, 150 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8fa8c1977f..0c832d37d6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,7 +10,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/) and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). ## [Unreleased] - +### Fixed +- [Spring] Inject CucumberContextConfiguration constructor dependencies ([#2664](https://github.com/cucumber/cucumber-jvm/pull/2664) M.P. Korstanje) + ## [7.10.0] - 2022-12-11 ### Added - Enabled reproducible builds ([#2641](https://github.com/cucumber/cucumber-jvm/issues/2641) Hervé Boutemy ) diff --git a/cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java b/cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java index 9afb5bc428..fc18b8e898 100644 --- a/cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java +++ b/cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java @@ -15,7 +15,7 @@ import java.util.Deque; import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE; -import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_NO; +import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_CONSTRUCTOR; class TestContextAdaptor { @@ -92,11 +92,25 @@ private void createAndPrepareTestInstance() { // using their default constructor and now allow them to be injected // into other step definition classes. try { - Class delegateTestClass = delegate.getTestContext().getTestClass(); - Object delegateTestInstance = applicationContext.getBeanFactory().autowire(delegateTestClass, AUTOWIRE_NO, - false); - delegate.prepareTestInstance(delegateTestInstance); - this.delegateTestInstance = delegateTestInstance; + Class beanClass = delegate.getTestContext().getTestClass(); + + ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory(); + // Note: By providing AUTOWIRE_CONSTRUCTOR the + // AbstractAutowireCapableBeanFactory does not invoke + // 'populateBean' and effectively creates a raw bean. + Object bean = beanFactory.autowire(beanClass, AUTOWIRE_CONSTRUCTOR, false); + + // But it works out well for us. Because now the + // DependencyInjectionTestExecutionListener will invoke + // 'autowireBeanProperties' which will populate the bean. + delegate.prepareTestInstance(bean); + + // Because the bean is created by a factory, it is not added to + // the application context yet. + CucumberTestContext scenarioScope = CucumberTestContext.getInstance(); + scenarioScope.put(beanClass.getName(), bean); + + this.delegateTestInstance = bean; } catch (Exception e) { throw new CucumberBackendException(e.getMessage(), e); } diff --git a/cucumber-spring/src/test/java/io/cucumber/spring/TestTestContextAdaptorTest.java b/cucumber-spring/src/test/java/io/cucumber/spring/TestTestContextAdaptorTest.java index 665ccb2f81..ddf37ce485 100644 --- a/cucumber-spring/src/test/java/io/cucumber/spring/TestTestContextAdaptorTest.java +++ b/cucumber-spring/src/test/java/io/cucumber/spring/TestTestContextAdaptorTest.java @@ -1,18 +1,30 @@ package io.cucumber.spring; import io.cucumber.core.backend.CucumberBackendException; +import io.cucumber.spring.beans.BellyBean; +import io.cucumber.spring.beans.DummyComponent; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; import org.mockito.InOrder; import org.mockito.Mock; import org.mockito.Mockito; import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.BeanNameAware; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.lang.NonNull; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.TestContextManager; import org.springframework.test.context.TestExecutionListener; import static java.util.Collections.singletonList; +import static org.junit.jupiter.api.Assertions.assertAll; +import static org.junit.jupiter.api.Assertions.assertDoesNotThrow; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doThrow; @@ -182,10 +194,125 @@ void invokesAllMethodsPriorIfAfterTestClassThrows() throws Exception { inOrder.verify(listener).afterTestClass(any()); } + @ParameterizedTest + @ValueSource(classes = { WithAutowiredDependency.class, WithConstructorDependency.class }) + void autowireAndPostProcessesOnlyOnce(Class testClass) { + TestContextManager manager = new TestContextManager(testClass); + TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(testClass)); + + assertAll( + () -> assertDoesNotThrow(adaptor::start), + () -> assertNotNull(manager.getTestContext().getTestInstance()), + () -> assertSame(manager.getTestContext().getTestInstance(), adaptor.getInstance(testClass)), + () -> assertEquals(1, adaptor.getInstance(testClass).autowiredCount()), + () -> assertEquals(1, adaptor.getInstance(testClass).postProcessedCount()), + () -> assertNotNull(adaptor.getInstance(testClass).getBelly()), + () -> assertNotNull(adaptor.getInstance(testClass).getDummyComponent()), + () -> assertDoesNotThrow(adaptor::stop)); + } + @CucumberContextConfiguration @ContextConfiguration("classpath:cucumber.xml") public static class SomeContextConfiguration { } + private interface Spy { + + int postProcessedCount(); + + int autowiredCount(); + + BellyBean getBelly(); + + DummyComponent getDummyComponent(); + + } + + @CucumberContextConfiguration + @ContextConfiguration("classpath:cucumber.xml") + public static class WithAutowiredDependency implements BeanNameAware, Spy { + + @Autowired + BellyBean belly; + + int postProcessedCount = 0; + int autowiredCount = 0; + + private DummyComponent dummyComponent; + + @Autowired + public void setDummyComponent(DummyComponent dummyComponent) { + this.dummyComponent = dummyComponent; + this.autowiredCount++; + } + + @Override + public void setBeanName(@NonNull String ignored) { + postProcessedCount++; + } + + @Override + public int postProcessedCount() { + return postProcessedCount; + } + + @Override + public int autowiredCount() { + return autowiredCount; + } + + @Override + public BellyBean getBelly() { + return belly; + } + + @Override + public DummyComponent getDummyComponent() { + return dummyComponent; + } + } + + @CucumberContextConfiguration + @ContextConfiguration("classpath:cucumber.xml") + public static class WithConstructorDependency implements BeanNameAware, Spy { + + final BellyBean belly; + final DummyComponent dummyComponent; + + int postProcessedCount = 0; + int autowiredCount = 0; + + public WithConstructorDependency(BellyBean belly, DummyComponent dummyComponent) { + this.belly = belly; + this.dummyComponent = dummyComponent; + this.autowiredCount++; + } + + @Override + public void setBeanName(@NonNull String ignored) { + postProcessedCount++; + } + + @Override + public int postProcessedCount() { + return postProcessedCount; + } + + @Override + public int autowiredCount() { + return autowiredCount; + } + + @Override + public BellyBean getBelly() { + return belly; + } + + @Override + public DummyComponent getDummyComponent() { + return dummyComponent; + } + } + }