Skip to content

Commit 09eac66

Browse files
committed
Merge pull request #711 from brasmusson/spring-stepdef-injection-error
Fix the glue class autowiring, transaction and cucumber-glue scope issues of the spring module
2 parents 8e43c11 + 4879f9e commit 09eac66

34 files changed

+512
-173
lines changed

examples/spring-txn/src/test/java/cucumber/examples/spring/txn/RunCukesTest.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,6 @@
55
import org.junit.runner.RunWith;
66

77
@RunWith(Cucumber.class)
8-
@CucumberOptions(glue = {"cucumber.examples.spring.txn", "cucumber.runtime.java.spring.hooks"})
8+
@CucumberOptions(glue = {"cucumber.examples.spring.txn", "cucumber.api.spring"})
99
public class RunCukesTest {
1010
}

examples/spring-txn/src/test/java/cucumber/examples/spring/txn/UserStepdefs.java

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
import org.springframework.beans.factory.annotation.Autowired;
55
import org.springframework.test.context.ContextConfiguration;
66
import org.springframework.test.context.web.WebAppConfiguration;
7+
import org.springframework.transaction.support.TransactionSynchronizationManager;
8+
9+
import static org.junit.Assert.assertTrue;
710

811
import java.util.List;
912

@@ -31,5 +34,7 @@ public void a_User_has_posted_the_following_messages(List<Message> messages) thr
3134
m.setAuthor(user);
3235
messageRepository.save(m);
3336
}
37+
assertTrue("No transaction is active",
38+
TransactionSynchronizationManager.isActualTransactionActive());
3439
}
3540
}

examples/spring-txn/src/test/resources/cucumber/examples/spring/txn/see_messages.feature

-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
@txn
21
Feature: See Messages
32

43
Scenario: See another user's messages
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,41 @@
11
package cucumber.runtime.java.spring;
22

3-
import java.io.PrintWriter;
4-
import java.io.StringWriter;
5-
import java.util.Collection;
6-
import java.util.HashMap;
7-
import java.util.HashSet;
8-
import java.util.Map;
9-
3+
import cucumber.runtime.CucumberException;
4+
import cucumber.runtime.java.ObjectFactory;
5+
import org.springframework.beans.BeansException;
106
import org.springframework.beans.factory.config.BeanDefinition;
117
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
128
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
139
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
1410
import org.springframework.context.ConfigurableApplicationContext;
15-
import org.springframework.context.support.GenericXmlApplicationContext;
11+
import org.springframework.context.support.ClassPathXmlApplicationContext;
12+
import org.springframework.context.support.GenericApplicationContext;
1613
import org.springframework.test.context.ContextConfiguration;
1714
import org.springframework.test.context.ContextHierarchy;
1815
import org.springframework.test.context.TestContextManager;
1916

20-
import cucumber.runtime.CucumberException;
21-
import cucumber.runtime.java.ObjectFactory;
17+
import java.lang.annotation.Annotation;
18+
import java.util.Collection;
19+
import java.util.HashSet;
2220

2321
/**
2422
* Spring based implementation of ObjectFactory.
2523
* <p/>
2624
* <p>
2725
* <ul>
28-
* <li>It uses TestContextManager to create and prepare test instances. Configuration via: @ContextConfiguration
26+
* <li>It uses TestContextManager to manage the spring context.
27+
* Configuration via: @ContextConfiguration or @ContextHierarcy
28+
* At least on step definition class needs to have a @ContextConfiguration or
29+
* @ContextHierarchy annotation. If more that one step definition class has such
30+
* an annotation, the annotations must be equal on the different step definition
31+
* classes. If no step definition class with @ContextConfiguration or
32+
* @ContextHierarcy is found, it will try to load cucumber.xml from the classpath.
2933
* </li>
30-
* <li>It also uses a context which contains the step definitions and is reloaded for each
31-
* scenario.</li>
34+
* <li>The step definitions class with @ContextConfiguration or @ContextHierarchy
35+
* annotation, may also have a @WebAppConfiguration or @DirtiesContext annotation.
36+
* </li>
37+
* <li>The step definitions added to the TestContextManagers context and
38+
* is reloaded for each scenario.</li>
3239
* </ul>
3340
* </p>
3441
* <p/>
@@ -39,137 +46,168 @@
3946
*/
4047
public class SpringFactory implements ObjectFactory {
4148

42-
private static ConfigurableApplicationContext applicationContext;
43-
private static ConfigurableListableBeanFactory beanFactory;
49+
private ConfigurableListableBeanFactory beanFactory;
50+
private CucumberTestContextManager testContextManager;
4451

4552
private final Collection<Class<?>> stepClasses = new HashSet<Class<?>>();
46-
private final Map<Class<?>, TestContextManager> contextManagersByClass = new HashMap<Class<?>, TestContextManager>();
53+
private Class<?> stepClassWithSpringContext = null;
4754

4855
public SpringFactory() {
4956
}
5057

51-
static {
52-
applicationContext = new GenericXmlApplicationContext("cucumber/runtime/java/spring/cucumber-glue.xml");
53-
applicationContext.registerShutdownHook();
54-
beanFactory = applicationContext.getBeanFactory();
55-
}
56-
5758
@Override
5859
public void addClass(final Class<?> stepClass) {
5960
if (!stepClasses.contains(stepClass)) {
61+
if (dependsOnSpringContext(stepClass)) {
62+
if (stepClassWithSpringContext == null) {
63+
stepClassWithSpringContext = stepClass;
64+
} else {
65+
checkAnnotationsEqual(stepClassWithSpringContext, stepClass);
66+
}
67+
}
6068
stepClasses.add(stepClass);
6169

62-
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) applicationContext.getAutowireCapableBeanFactory();
63-
BeanDefinition beanDefinition = BeanDefinitionBuilder
64-
.genericBeanDefinition(stepClass)
65-
.setScope(GlueCodeScope.NAME)
66-
.getBeanDefinition();
67-
registry.registerBeanDefinition(stepClass.getName(), beanDefinition);
6870
}
6971
}
7072

73+
private void checkAnnotationsEqual(Class<?> stepClassWithSpringContext, Class<?> stepClass) {
74+
Annotation[] annotations1 = stepClassWithSpringContext.getAnnotations();
75+
Annotation[] annotations2 = stepClass.getAnnotations();
76+
if (annotations1.length != annotations2.length) {
77+
throw new CucumberException("Annotations differs on glue classes found: " +
78+
stepClassWithSpringContext.getName() + ", " +
79+
stepClass.getName());
80+
}
81+
for (Annotation annotation : annotations1) {
82+
if (!isAnnotationInArray(annotation, annotations2)) {
83+
throw new CucumberException("Annotations differs on glue classes found: " +
84+
stepClassWithSpringContext.getName() + ", " +
85+
stepClass.getName());
86+
}
87+
}
88+
}
89+
90+
private boolean isAnnotationInArray(Annotation annotation, Annotation[] annotations) {
91+
for (Annotation annotationFromArray: annotations) {
92+
if (annotation.equals(annotationFromArray)) {
93+
return true;
94+
}
95+
}
96+
return false;
97+
}
98+
7199
@Override
72100
public void start() {
101+
if (stepClassWithSpringContext != null) {
102+
testContextManager = new CucumberTestContextManager(stepClassWithSpringContext);
103+
} else {
104+
if (beanFactory == null) {
105+
beanFactory = createFallbackContext();
106+
}
107+
}
108+
notifyContextManagerAboutTestClassStarted();
109+
if (beanFactory == null || isNewContextCreated()) {
110+
beanFactory = testContextManager.getBeanFactory();
111+
for (Class<?> stepClass : stepClasses) {
112+
registerStepClassBeanDefinition(beanFactory, stepClass);
113+
}
114+
}
73115
GlueCodeContext.INSTANCE.start();
74116
}
75117

76-
@Override
77-
public void stop() {
78-
notifyContextManagersAboutTestClassFinished();
79-
80-
GlueCodeContext.INSTANCE.stop();
81-
beanFactory.destroySingletons();
118+
@SuppressWarnings("resource")
119+
private ConfigurableListableBeanFactory createFallbackContext() {
120+
ConfigurableApplicationContext applicationContext;
121+
if (getClass().getClassLoader().getResource("cucumber.xml") != null) {
122+
applicationContext = new ClassPathXmlApplicationContext("cucumber.xml");
123+
} else {
124+
applicationContext = new GenericApplicationContext();
125+
}
126+
applicationContext.registerShutdownHook();
127+
ConfigurableListableBeanFactory beanFactory = applicationContext.getBeanFactory();
128+
beanFactory.registerScope(GlueCodeScope.NAME, new GlueCodeScope());
129+
for (Class<?> stepClass : stepClasses) {
130+
registerStepClassBeanDefinition(beanFactory, stepClass);
131+
}
132+
return beanFactory;
82133
}
83134

84-
private void notifyContextManagersAboutTestClassFinished() {
85-
Map<Class<?>, Exception> exceptionsThrown = new HashMap<Class<?>, Exception>();
86-
87-
for (Map.Entry<Class<?>, TestContextManager> classTestContextManagerEntry : contextManagersByClass
88-
.entrySet()) {
135+
private void notifyContextManagerAboutTestClassStarted() {
136+
if (testContextManager != null) {
89137
try {
90-
classTestContextManagerEntry.getValue().afterTestClass();
138+
testContextManager.beforeTestClass();
91139
} catch (Exception e) {
92-
exceptionsThrown.put(classTestContextManagerEntry.getKey(), e);
140+
throw new CucumberException(e.getMessage(), e);
93141
}
94142
}
95-
96-
contextManagersByClass.clear();
97-
98-
rethrowExceptionsIfAny(exceptionsThrown);
99143
}
100144

101-
private void rethrowExceptionsIfAny(Map<Class<?>, Exception> exceptionsThrown) {
102-
if (exceptionsThrown.isEmpty()) {
103-
return;
145+
private boolean isNewContextCreated() {
146+
if (testContextManager == null) {
147+
return false;
104148
}
105-
106-
if (exceptionsThrown.size() == 1) {
107-
//ony one exception, throw an exception with the correct cause
108-
Exception e = exceptionsThrown.values().iterator().next();
109-
throw new CucumberException(e.getMessage(), e);
110-
}
111-
112-
//multiple exceptions but we can only have one cause, put relevant info in the exception message
113-
//to not lose the interesting data
114-
throw new CucumberException(getMultipleExceptionMessage(exceptionsThrown));
149+
return !beanFactory.equals(testContextManager.getBeanFactory());
115150
}
116151

117-
private String getMultipleExceptionMessage(Map<Class<?>, Exception> exceptionsThrow) {
118-
StringBuilder exceptionsThrown = new StringBuilder(1000);
119-
exceptionsThrown.append("Multiple exceptions occurred during processing of the TestExecutionListeners\n\n");
120-
121-
for (Map.Entry<Class<?>, Exception> classExceptionEntry : exceptionsThrow.entrySet()) {
122-
exceptionsThrown.append("Exception during processing of TestExecutionListeners of ");
123-
exceptionsThrown.append(classExceptionEntry.getKey());
124-
exceptionsThrown.append('\n');
125-
exceptionsThrown.append(classExceptionEntry.getValue().toString());
126-
exceptionsThrown.append('\n');
152+
private void registerStepClassBeanDefinition(ConfigurableListableBeanFactory beanFactory, Class<?> stepClass) {
153+
BeanDefinitionRegistry registry = (BeanDefinitionRegistry) beanFactory;
154+
BeanDefinition beanDefinition = BeanDefinitionBuilder
155+
.genericBeanDefinition(stepClass)
156+
.setScope(GlueCodeScope.NAME)
157+
.getBeanDefinition();
158+
registry.registerBeanDefinition(stepClass.getName(), beanDefinition);
159+
}
127160

128-
StringWriter stackTraceStringWriter = new StringWriter();
129-
PrintWriter stackTracePrintWriter = new PrintWriter(stackTraceStringWriter);
130-
classExceptionEntry.getValue().printStackTrace(stackTracePrintWriter);
131-
exceptionsThrown.append(stackTraceStringWriter.toString());
132-
exceptionsThrown.append('\n');
161+
@Override
162+
public void stop() {
163+
notifyContextManagerAboutTestClassFinished();
164+
GlueCodeContext.INSTANCE.stop();
165+
}
133166

167+
private void notifyContextManagerAboutTestClassFinished() {
168+
if (testContextManager != null) {
169+
try {
170+
testContextManager.afterTestClass();
171+
} catch (Exception e) {
172+
throw new CucumberException(e.getMessage(), e);
173+
}
134174
}
135-
136-
return exceptionsThrown.toString();
137175
}
138176

139177
@Override
140178
public <T> T getInstance(final Class<T> type) {
141-
if (!beanFactory.containsSingleton(type.getName())) {
142-
beanFactory.registerSingleton(type.getName(), getTestInstance(type));
179+
try {
180+
return beanFactory.getBean(type);
181+
} catch (BeansException e) {
182+
throw new CucumberException(e.getMessage(), e);
143183
}
144-
145-
return applicationContext.getBean(type);
146184
}
147185

148-
private <T> T getTestInstance(final Class<T> type) {
149-
try {
150-
T instance = createTest(type);
186+
private boolean dependsOnSpringContext(Class<?> type) {
187+
return type.isAnnotationPresent(ContextConfiguration.class)
188+
|| type.isAnnotationPresent(ContextHierarchy.class);
189+
}
190+
}
151191

152-
if (dependsOnSpringContext(type)) {
153-
TestContextManager contextManager = new TestContextManager(type);
154-
contextManager.prepareTestInstance(instance);
155-
contextManager.beforeTestClass();
192+
class CucumberTestContextManager extends TestContextManager {
156193

157-
contextManagersByClass.put(type, contextManager);
158-
}
194+
public CucumberTestContextManager(Class<?> testClass) {
195+
super(testClass);
196+
registerGlueCodeScope(getContext());
197+
}
159198

160-
return instance;
161-
} catch (Exception e) {
162-
throw new CucumberException(e.getMessage(), e);
163-
}
199+
public ConfigurableListableBeanFactory getBeanFactory() {
200+
return getContext().getBeanFactory();
164201
}
165202

166-
@SuppressWarnings("unchecked")
167-
protected <T> T createTest(Class<T> type) throws Exception {
168-
return (T) type.getConstructors()[0].newInstance();
203+
private ConfigurableApplicationContext getContext() {
204+
return (ConfigurableApplicationContext)getTestContext().getApplicationContext();
169205
}
170206

171-
private boolean dependsOnSpringContext(Class<?> type) {
172-
return type.isAnnotationPresent(ContextConfiguration.class)
173-
|| type.isAnnotationPresent(ContextHierarchy.class);
207+
private void registerGlueCodeScope(ConfigurableApplicationContext context) {
208+
do {
209+
context.getBeanFactory().registerScope(GlueCodeScope.NAME, new GlueCodeScope());
210+
context = (ConfigurableApplicationContext)context.getParent();
211+
} while (context != null);
174212
}
175213
}

spring/src/main/resources/cucumber/runtime/java/spring/cucumber-glue.xml

-18
This file was deleted.

spring/src/test/java/cucumber/runtime/java/spring/NonSpringGlue.java

-17
This file was deleted.

spring/src/test/java/cucumber/runtime/java/spring/RunCukesTest.java

-8
This file was deleted.

0 commit comments

Comments
 (0)