Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[Spring] Inject CucumberContextConfiguration constructor dependencies #2664

Merged
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
@@ -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;
}
}

}