Skip to content

Commit

Permalink
[Spring] Support constructor dependency @CucumberContextConfiguration…
Browse files Browse the repository at this point in the history
… classes

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 4b8d712
Show file tree
Hide file tree
Showing 2 changed files with 140 additions and 3 deletions.
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 @@ -93,9 +93,18 @@ private void createAndPrepareTestInstance() {
// into other step definition classes.
try {
Class<?> delegateTestClass = delegate.getTestContext().getTestClass();
Object delegateTestInstance = applicationContext.getBeanFactory().autowire(delegateTestClass, AUTOWIRE_NO,
false);

ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
// Note: By providing AUTOWIRE_CONSTRUCTOR the AbstractAutowireCapableBeanFactory
// does not invoke 'populateBean' and effectively creates a raw bean.
Object delegateTestInstance = beanFactory.autowire(delegateTestClass, AUTOWIRE_CONSTRUCTOR, false);
// But it works out well for us. Because now the DependencyInjectionTestExecutionListener
// will invoke 'autowireBeanProperties' which will populate the bean.
delegate.prepareTestInstance(delegateTestInstance);

CucumberTestContext scenarioScope = CucumberTestContext.getInstance();
scenarioScope.put(delegateTestClass.getName(), delegateTestInstance);

this.delegateTestInstance = delegateTestInstance;
} 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,126 @@ 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 4b8d712

Please sign in to comment.