Skip to content

Commit

Permalink
Invoke all phases
Browse files Browse the repository at this point in the history
  • Loading branch information
mpkorstanje committed Dec 11, 2022
1 parent 36e0ef0 commit fad415e
Show file tree
Hide file tree
Showing 3 changed files with 240 additions and 61 deletions.
124 changes: 104 additions & 20 deletions cucumber-spring/src/main/java/io/cucumber/spring/TestContextAdaptor.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,12 @@
import org.springframework.test.context.TestContextManager;

import java.lang.reflect.Method;
import java.util.ArrayDeque;
import java.util.Collection;
import java.util.Deque;

import static io.cucumber.spring.CucumberTestContext.SCOPE_CUCUMBER_GLUE;
import static org.springframework.beans.factory.config.AutowireCapableBeanFactory.AUTOWIRE_NO;

class TestContextAdaptor {

Expand All @@ -21,6 +24,7 @@ class TestContextAdaptor {
private final TestContextManager delegate;
private final ConfigurableApplicationContext applicationContext;
private final Collection<Class<?>> glueClasses;
private final Deque<Runnable> stopInvocations = new ArrayDeque<>();
private Object delegateTestInstance;

TestContextAdaptor(
Expand All @@ -44,24 +48,70 @@ public final void start() {
registerGlueCodeScope(applicationContext);
registerStepClassBeanDefinitions(applicationContext.getBeanFactory());
}
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestClass);
notifyContextManagerAboutBeforeTestClass();
CucumberTestContext.getInstance().start();
stopInvocations.push(this::stopCucumberTestContext);
startCucumberTestContext();
stopInvocations.push(this::disposeTestInstance);
createAndPrepareTestInstance();
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestMethod);
notifyTestContextManagerAboutBeforeTestMethod();
stopInvocations.push(this::notifyTestContextManagerAboutAfterTestExecution);
notifyTestContextManagerAboutBeforeExecution();
}

private void startCucumberTestContext() {
CucumberTestContext.getInstance().start();
}


private void notifyContextManagerAboutBeforeTestClass() {
try {
delegate.beforeTestClass();
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

private void notifyTestContextManagerAboutBeforeTestMethod() {
try {
Method dummyMethod = getDummyMethod();
delegate.beforeTestMethod(delegateTestInstance, dummyMethod);
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

private void createAndPrepareTestInstance() {
// Unlike JUnit, Cucumber does not have a single test class.
// Springs TestContext however assumes we do, and we are expected to
// create an instance of it using the default constructor.
//
// Users of Cucumber would however like to inject their step
// definition classes into other step definition classes. This requires
// that the test instance exists in the application context as a bean.
//
// Normally when a bean is pulled from the application context with
// getBean it is also autowired. This will however conflict with
// Springs DependencyInjectionTestExecutionListener. So we create
// a raw bean here.
//
// This probably free from side effects, but at some point in the
// future we may have to accept that the only way forward is to
// construct instances annotated with @CucumberContextConfiguration
// using their default constructor and now allow them to be injected
// into other step definition classes.
try {
Class<?> delegateTestClass = delegate.getTestContext().getTestClass();
delegateTestInstance = applicationContext.getBean(delegateTestClass);
Object delegateTestInstance = applicationContext.getBeanFactory().autowire(delegateTestClass, AUTOWIRE_NO, false);
delegate.prepareTestInstance(delegateTestInstance);
Method dummyMethod = TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
delegate.beforeTestMethod(delegateTestInstance, dummyMethod);
this.delegateTestInstance = delegateTestInstance;
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

final void registerGlueCodeScope(ConfigurableApplicationContext context) {
private void registerGlueCodeScope(ConfigurableApplicationContext context) {
while (context != null) {
ConfigurableListableBeanFactory beanFactory = context.getBeanFactory();
// Scenario scope may have already been registered by another
Expand All @@ -74,15 +124,15 @@ final void registerGlueCodeScope(ConfigurableApplicationContext context) {
}
}

private void notifyContextManagerAboutBeforeTestClass() {
private void notifyTestContextManagerAboutBeforeExecution() {
try {
delegate.beforeTestClass();
delegate.beforeTestExecution(delegateTestInstance, getDummyMethod());
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

final void registerStepClassBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
private void registerStepClassBeanDefinitions(ConfigurableListableBeanFactory beanFactory) {
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
for (Class<?> glue : glueClasses) {
registerStepClassBeanDefinition(registry, glue);
Expand All @@ -103,18 +153,20 @@ private void registerStepClassBeanDefinition(BeanDefinitionRegistry registry, Cl
}

public final void stop() {
// Don't invoke after test method when before test class was not invoked
// this is implicit in the existence of an active the test context
// session. This is not ideal, but Cucumber only supports 1 set of
// before/after semantics while JUnit and Spring have 2 sets.
if (CucumberTestContext.getInstance().isActive()) {
if (delegateTestInstance != null) {
notifyTestContextManagerAboutAfterTestMethod();
delegateTestInstance = null;
CucumberBackendException lastException = null;
for (Runnable stopInvocation : stopInvocations) {
try {
stopInvocation.run();
} catch (CucumberBackendException e) {
if (lastException != null) {
e.addSuppressed(lastException);
}
lastException = e;
}
CucumberTestContext.getInstance().stop();
}
notifyTestContextManagerAboutAfterTestClass();
if (lastException != null) {
throw lastException;
}
}

private void notifyTestContextManagerAboutAfterTestClass() {
Expand All @@ -125,11 +177,35 @@ private void notifyTestContextManagerAboutAfterTestClass() {
}
}

private void stopCucumberTestContext() {
CucumberTestContext.getInstance().stop();
}

private void disposeTestInstance() {
delegateTestInstance = null;
}

private void notifyTestContextManagerAboutAfterTestMethod() {
try {
Object delegateTestInstance = delegate.getTestContext().getTestInstance();
Method dummyMethod = TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
delegate.afterTestMethod(delegateTestInstance, dummyMethod, null);
// Cucumber tests can throw exceptions, but we can't currently
// get at them. So we provide null intentionally.
// Cucumber also doesn't a single test method, so we provide a
// dummy instead.
delegate.afterTestMethod(delegateTestInstance, getDummyMethod(), null);
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
}

private void notifyTestContextManagerAboutAfterTestExecution() {
try {
Object delegateTestInstance = delegate.getTestContext().getTestInstance();
// Cucumber tests can throw exceptions, but we can't currently
// get at them. So we provide null intentionally.
// Cucumber also doesn't a single test method, so we provide a
// dummy instead.
delegate.afterTestExecution(delegateTestInstance, getDummyMethod(), null);
} catch (Exception e) {
throw new CucumberBackendException(e.getMessage(), e);
}
Expand All @@ -139,6 +215,14 @@ final <T> T getInstance(Class<T> type) {
return applicationContext.getBean(type);
}

private Method getDummyMethod() {
try {
return TestContextAdaptor.class.getMethod("cucumberDoesNotHaveASingleTestMethod");
} catch (NoSuchMethodException e) {
throw new RuntimeException(e);
}
}

public void cucumberDoesNotHaveASingleTestMethod() {

}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -347,15 +347,10 @@ void shouldBeStoppableWhenFacedWithMissingContextConfiguration() {
assertDoesNotThrow(factory::stop);
}

@ParameterizedTest
@ValueSource(classes = {
FailedBeforeTestClassContextConfiguration.class,
FailedBeforeTestMethodContextConfiguration.class,
FailedTestInstanceContextConfiguration.class
})
void shouldBeStoppableWhenFacedWithFailedApplicationContext(Class<?> contextConfiguration) {
@Test
void shouldBeStoppableWhenFacedWithFailedApplicationContext() {
final ObjectFactory factory = new SpringFactory();
factory.addClass(contextConfiguration);
factory.addClass(FailedTestInstanceCreation.class);

assertThrows(CucumberBackendException.class, factory::start);
assertDoesNotThrow(factory::stop);
Expand Down Expand Up @@ -414,40 +409,9 @@ public static class WithoutContextConfiguration {

@CucumberContextConfiguration
@ContextConfiguration("classpath:cucumber.xml")
@TestExecutionListeners(FailedBeforeTestClassContextConfiguration.FailingListener.class)
public static class FailedBeforeTestClassContextConfiguration {

public static class FailingListener implements TestExecutionListener {

@Override
public void beforeTestClass(TestContext testContext) throws Exception {
throw new StubException();
}

}

}

@CucumberContextConfiguration
@ContextConfiguration("classpath:cucumber.xml")
@TestExecutionListeners(FailedBeforeTestMethodContextConfiguration.FailingListener.class)
public static class FailedBeforeTestMethodContextConfiguration {

public static class FailingListener implements TestExecutionListener {

@Override
public void beforeTestMethod(TestContext testContext) throws Exception {
throw new StubException();
}

}

}
@CucumberContextConfiguration
@ContextConfiguration("classpath:cucumber.xml")
public static class FailedTestInstanceContextConfiguration {
public static class FailedTestInstanceCreation {

public FailedTestInstanceContextConfiguration() {
public FailedTestInstanceCreation() {
throw new RuntimeException();
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
package io.cucumber.spring;

import io.cucumber.core.backend.CucumberBackendException;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InOrder;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
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.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.inOrder;

@ExtendWith(MockitoExtension.class)
public class TestTestContextAdaptorTest {

@Mock
TestExecutionListener listener;

@AfterEach
void verifyNoMoroInteractions(){
Mockito.verifyNoMoreInteractions(listener);
}

@Test
void invokesAllLiveCycleHooks() throws Exception {
TestContextManager manager = new TestContextManager(SpringContextConfiguration.class);
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(SpringContextConfiguration.class));
manager.registerTestExecutionListeners(listener);
InOrder inOrder = inOrder(listener);

adaptor.start();
inOrder.verify(listener).beforeTestClass(any());
inOrder.verify(listener).prepareTestInstance(any());
inOrder.verify(listener).beforeTestMethod(any());
inOrder.verify(listener).beforeTestExecution(any());


adaptor.stop();
inOrder.verify(listener).afterTestExecution(any());
inOrder.verify(listener).afterTestMethod(any());
inOrder.verify(listener).afterTestClass(any());
}

@Test
void invokesAfterClassIfBeforeClassFailed() throws Exception {
TestContextManager manager = new TestContextManager(SpringContextConfiguration.class);
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(SpringContextConfiguration.class));
manager.registerTestExecutionListeners(listener);
InOrder inOrder = inOrder(listener);

doThrow(new RuntimeException()).when(listener).beforeTestClass(any());

assertThrows(CucumberBackendException.class, adaptor::start);
inOrder.verify(listener).beforeTestClass(any());

adaptor.stop();
inOrder.verify(listener).afterTestClass(any());
}


@Test
void invokesAfterClassIfPrepareTestInstanceFailed() throws Exception {
TestContextManager manager = new TestContextManager(SpringContextConfiguration.class);
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(SpringContextConfiguration.class));
manager.registerTestExecutionListeners(listener);
InOrder inOrder = inOrder(listener);

doThrow(new RuntimeException()).when(listener).prepareTestInstance(any());

assertThrows(CucumberBackendException.class, adaptor::start);
inOrder.verify(listener).beforeTestClass(any());

adaptor.stop();
inOrder.verify(listener).afterTestClass(any());
}


@Test
void invokesAfterMethodIfBeforeMethodThrows() throws Exception {
TestContextManager manager = new TestContextManager(SpringContextConfiguration.class);
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(SpringContextConfiguration.class));
manager.registerTestExecutionListeners(listener);
InOrder inOrder = inOrder(listener);

doThrow(new RuntimeException()).when(listener).beforeTestMethod(any());

assertThrows(CucumberBackendException.class, adaptor::start);
inOrder.verify(listener).beforeTestClass(any());
inOrder.verify(listener).prepareTestInstance(any());
inOrder.verify(listener).beforeTestMethod(any());

adaptor.stop();
inOrder.verify(listener).afterTestMethod(any());
inOrder.verify(listener).afterTestClass(any());
}

@Test
void invokesAfterTestExecutionIfBeforeTestExecutionThrows() throws Exception {
TestContextManager manager = new TestContextManager(SpringContextConfiguration.class);
TestContextAdaptor adaptor = new TestContextAdaptor(manager, singletonList(SpringContextConfiguration.class));
manager.registerTestExecutionListeners(listener);
InOrder inOrder = inOrder(listener);

doThrow(new RuntimeException()).when(listener).beforeTestExecution(any());

assertThrows(CucumberBackendException.class, adaptor::start);
inOrder.verify(listener).beforeTestClass(any());
inOrder.verify(listener).prepareTestInstance(any());
inOrder.verify(listener).beforeTestMethod(any());

adaptor.stop();
inOrder.verify(listener).afterTestExecution(any());
inOrder.verify(listener).afterTestMethod(any());
inOrder.verify(listener).afterTestClass(any());
}

@CucumberContextConfiguration
@ContextConfiguration("classpath:cucumber.xml")
public static class SpringContextConfiguration {

}

}

0 comments on commit fad415e

Please sign in to comment.