Skip to content

Commit

Permalink
[Spring] Inject CucumberContextConfiguration constructor dependencies
Browse files Browse the repository at this point in the history
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
  • Loading branch information
mpkorstanje committed Dec 16, 2022
1 parent fe2d667 commit 1a55e41
Show file tree
Hide file tree
Showing 3 changed files with 150 additions and 7 deletions.
4 changes: 3 additions & 1 deletion CHANGELOG.md
Expand Up @@ -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 )
Expand Down
Expand Up @@ -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 {

Expand Down Expand Up @@ -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);
}
Expand Down
@@ -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;
Expand Down Expand Up @@ -182,10 +194,125 @@ void invokesAllMethodsPriorIfAfterTestClassThrows() throws Exception {
inOrder.verify(listener).afterTestClass(any());
}

@ParameterizedTest
@ValueSource(classes = { WithAutowiredDependency.class, WithConstructorDependency.class })
void autowireAndPostProcessesOnlyOnce(Class<? extends Spy> 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;
}
}

}

0 comments on commit 1a55e41

Please sign in to comment.