Skip to content

Commit 8f09905

Browse files
committed
[Spring] Remove cucumber.xml and implied context configuration
The preferred way to use `cucumber-spring` is to annotate a class with both `@CucumberContextConfiguration` and a Spring context configuration annotation such as `@ContextConfiguration`, `@SpringBootTest`, ect. Previously Cucumber would support the discovery of context configuration on any step definition class. This requires a significant amount of code and intimate knowledge of the Spring Framework. By restricting this too only classes annotated with `@CucumberContextConfiguration` we remove this complexity and simply pass the annotated class to Spring `TestContextManager` framework. If no context configuration is available Cucumber currently also supports reading the application context from a magical `cucumber.xml` file or should none exist an empty application context. Both these fallback strategies tend to hide user errors. And removing them will provide more clarity and also removes a nugget of complexity.
1 parent 5b73489 commit 8f09905

File tree

4 files changed

+124
-319
lines changed

4 files changed

+124
-319
lines changed

spring/src/main/java/io/cucumber/spring/SpringFactory.java

Lines changed: 93 additions & 157 deletions
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,15 @@
22

33
import io.cucumber.core.backend.CucumberBackendException;
44
import io.cucumber.core.backend.ObjectFactory;
5-
import io.cucumber.core.logging.Logger;
6-
import io.cucumber.core.logging.LoggerFactory;
75
import org.apiguardian.api.API;
86
import org.springframework.beans.BeansException;
97
import org.springframework.stereotype.Component;
8+
import org.springframework.test.annotation.DirtiesContext;
109
import org.springframework.test.context.BootstrapWith;
1110
import org.springframework.test.context.ContextConfiguration;
1211
import org.springframework.test.context.ContextHierarchy;
1312
import org.springframework.test.context.TestContextManager;
13+
import org.springframework.test.context.web.WebAppConfiguration;
1414

1515
import java.lang.annotation.Annotation;
1616
import java.util.ArrayDeque;
@@ -20,53 +20,58 @@
2020
import java.util.HashSet;
2121
import java.util.Set;
2222

23-
import static io.cucumber.spring.TestContextAdaptor.createClassPathXmlApplicationContextAdaptor;
24-
import static io.cucumber.spring.TestContextAdaptor.createGenericApplicationContextAdaptor;
2523
import static io.cucumber.spring.TestContextAdaptor.createTestContextManagerAdaptor;
26-
import static java.util.Arrays.asList;
2724

2825
/**
2926
* Spring based implementation of ObjectFactory.
3027
* <p>
3128
* Application beans are accessible from the step definitions using autowiring
3229
* (with annotations).
3330
* <p>
34-
* SpringFactory uses TestContextManager to manage the spring context. The step definitions are added to the
35-
* TestContextManagers context and the context is reloaded for each scenario.
36-
* <p>
37-
* The spring context can be configured by:
38-
* <ul>
39-
* <li>Annotating one step definition with: @{@link ContextConfiguration}, @{@link ContextHierarchy}
40-
* or @{@link BootstrapWith}. This step definition can also be annotated
41-
* with @{@link org.springframework.test.context.web.WebAppConfiguration}
42-
* or @{@link org.springframework.test.annotation.DirtiesContext} annotation.
43-
* </li>
44-
* <li>Deprecated: If no step definition class with @ContextConfiguration or @ContextHierarchy
45-
* is found, it will try to load cucumber.xml from the classpath.</li>
46-
* </ul>
31+
* The spring context can be configured by annotating one glue class with a @{@link CucumberContextConfiguration} and
32+
* any one of the following @{@link ContextConfiguration}, @{@link ContextHierarchy} or @{@link BootstrapWith}.
33+
* This glue class can also be annotated with @{@link WebAppConfiguration} or @{@link DirtiesContext} annotation.
4734
* <p>
4835
* Notes:
4936
* <ul>
50-
* <li>
51-
* Step definitions should not be annotated with @{@link Component} or other annotations that mark it as eligible for
52-
* detection by classpath scanning. When a step definition class is annotated by @Component or an annotation that has
53-
* the @Component stereotype an exception will be thrown
54-
* </li>
55-
* <li>
56-
* If more that one glue class is used to configure the spring context an exception will be thrown.
57-
* </li>
37+
* <li>
38+
* SpringFactory uses Springs TestContextManager framework to manage the spring context. The class annotated
39+
* with {@code CucumberContextConfiguration} will be use to instantiate the {@link TestContextManager}.
40+
* </li>
41+
* <li>
42+
* If not exactly one glue class is annotated with {@code CucumberContextConfiguration} an exception will be
43+
* thrown.
44+
* </li>
45+
*
46+
* <li>
47+
* Step definitions should not be annotated with @{@link Component} or other annotations that mark it as
48+
* eligible for detection by classpath scanning. When a step definition class is annotated by @Component or an
49+
* annotation that has the @Component stereotype an exception will be thrown
50+
* </li>
5851
* </ul>
5952
*/
6053
@API(status = API.Status.STABLE)
6154
public final class SpringFactory implements ObjectFactory {
6255

63-
private static final Logger log = LoggerFactory.getLogger(SpringFactory.class);
64-
65-
private static final String CUCUMBER_XML = "cucumber.xml";
6656
private final Collection<Class<?>> stepClasses = new HashSet<>();
67-
private Class<?> stepClassWithSpringContext = null;
57+
private Class<?> withCucumberContextConfiguration = null;
6858
private TestContextAdaptor testContextAdaptor;
6959

60+
@Override
61+
public boolean addClass(final Class<?> stepClass) {
62+
if (stepClasses.contains(stepClass)) {
63+
return true;
64+
}
65+
66+
checkNoComponentAnnotations(stepClass);
67+
if (hasCucumberContextConfiguration(stepClass)) {
68+
checkOnlyOneClassHasCucumberContextConfiguration(stepClass);
69+
withCucumberContextConfiguration = stepClass;
70+
}
71+
stepClasses.add(stepClass);
72+
return true;
73+
}
74+
7075
private static void checkNoComponentAnnotations(Class<?> type) {
7176
for (Annotation annotation : type.getAnnotations()) {
7277
if (hasComponentAnnotation(annotation)) {
@@ -80,143 +85,49 @@ private static void checkNoComponentAnnotations(Class<?> type) {
8085
}
8186
}
8287

83-
private static boolean hasComponentAnnotation(Annotation annotation) {
84-
return hasAnnotation(annotation, Collections.singleton(Component.class));
85-
}
86-
87-
private static boolean hasAnnotation(Annotation annotation, Collection<Class<? extends Annotation>> desired) {
88-
Set<Class<? extends Annotation>> seen = new HashSet<>();
89-
Deque<Class<? extends Annotation>> toCheck = new ArrayDeque<>();
90-
toCheck.add(annotation.annotationType());
91-
92-
while (!toCheck.isEmpty()) {
93-
Class<? extends Annotation> annotationType = toCheck.pop();
94-
if (desired.contains(annotationType)) {
95-
return true;
96-
}
97-
98-
seen.add(annotationType);
99-
for (Annotation annotationTypesAnnotations : annotationType.getAnnotations()) {
100-
if (!seen.contains(annotationTypesAnnotations.annotationType())) {
101-
toCheck.add(annotationTypesAnnotations.annotationType());
102-
}
103-
}
104-
105-
}
106-
return false;
107-
}
108-
109-
@Deprecated
110-
private static boolean dependsOnSpringContext(Class<?> type) {
111-
for (Annotation annotation : type.getAnnotations()) {
112-
if (annotatedWithSupportedSpringRootTestAnnotations(annotation)) {
113-
return true;
114-
}
115-
}
116-
return false;
117-
}
118-
119-
@Deprecated
120-
private static boolean annotatedWithSupportedSpringRootTestAnnotations(Annotation type) {
121-
return hasAnnotation(type, asList(
122-
ContextConfiguration.class,
123-
ContextHierarchy.class,
124-
BootstrapWith.class
125-
));
88+
private static boolean hasCucumberContextConfiguration(Class<?> stepClass) {
89+
return stepClass.getAnnotation(CucumberContextConfiguration.class) != null;
12690
}
12791

128-
@Override
129-
public boolean addClass(final Class<?> stepClass) {
130-
if (!stepClasses.contains(stepClass)) {
131-
checkNoComponentAnnotations(stepClass);
132-
if (dependsOnSpringContext(stepClass) || hasCucumberContextConfiguration(stepClass)) {
133-
if (stepClassWithSpringContext != null) {
134-
throw new CucumberBackendException(String.format("" +
135-
"Glue class %1$s and %2$s both attempt to configure the spring context. Please ensure only one " +
136-
"glue class configures the spring context", stepClass, stepClassWithSpringContext
137-
));
138-
}
139-
140-
if (dependsOnSpringContext(stepClass) && !hasCucumberContextConfiguration(stepClass)) {
141-
log.warn(() -> String.format(
142-
"Glue class %1$s attempts to configure the spring context but was not annotated with %2$s.\n" +
143-
"Implicit configuration of the spring context is deprecated.\n" +
144-
"Please add the %2$s to %1$s", stepClass, CucumberContextConfiguration.class.getName()
145-
));
146-
}
147-
148-
stepClassWithSpringContext = stepClass;
149-
}
150-
stepClasses.add(stepClass);
92+
private void checkOnlyOneClassHasCucumberContextConfiguration(Class<?> stepClass) {
93+
if (withCucumberContextConfiguration != null) {
94+
throw new CucumberBackendException(String.format("" +
95+
"Glue class %1$s and %2$s are both annotated with @CucumberContextConfiguration.\n" +
96+
"Please ensure only one class configures the spring context",
97+
stepClass,
98+
withCucumberContextConfiguration
99+
));
151100
}
152-
return true;
153-
}
154-
155-
private boolean hasCucumberContextConfiguration(Class<?> stepClass) {
156-
return stepClass.getAnnotation(CucumberContextConfiguration.class) != null;
157101
}
158102

159-
160103
@Override
161104
public void start() {
162-
if (stepClassWithSpringContext != null) {
163-
// The application context created by the TestContextManager is
164-
// a singleton and reused between scenarios and shared between
165-
// threads.
166-
TestContextManager testContextManager = new TestContextManager(stepClassWithSpringContext);
167-
testContextAdaptor = createTestContextManagerAdaptor(testContextManager, stepClasses);
168-
} else if (getClass().getClassLoader().getResource(CUCUMBER_XML) == null) {
169-
warnAboutDeprecationOfGenericApplicationContext();
170-
// The generic fallback application context is not shared between
171-
// threads (because the spring factory is not shared) and not reused
172-
// between scenarios because we recreate it each time the spring
173-
// factory starts.
174-
testContextAdaptor = createGenericApplicationContextAdaptor(stepClasses);
175-
} else if (testContextAdaptor == null) {
176-
warnAboutDeprecationOfCucumberXml();
177-
178-
// The xml fallback application context is not shared between
179-
// threads (because the spring factory is not shared) but is reused
180-
// between scenarios.
181-
String[] configLocations = {CUCUMBER_XML};
182-
testContextAdaptor = createClassPathXmlApplicationContextAdaptor(configLocations, stepClasses);
105+
if (withCucumberContextConfiguration == null) {
106+
throw new CucumberBackendException("" +
107+
"Please annotate a glue class with some context configuration.\n" +
108+
"\n" +
109+
"For example:\n" +
110+
"\n" +
111+
" @CucumberContextConfiguration\n" +
112+
" @SpringBootTest(classes = TestConfig.class)\n" +
113+
" public class CucumberSpringConfiguration { }" +
114+
"\n" +
115+
"Or: \n" +
116+
"\n" +
117+
" @CucumberContextConfiguration\n" +
118+
" @ContextConfiguration( ... )\n" +
119+
" public class CucumberSpringConfiguration { }"
120+
);
183121
}
184-
testContextAdaptor.start();
185-
}
186-
187-
private void warnAboutDeprecationOfGenericApplicationContext() {
188-
log.warn(() -> "" +
189-
"Glue glue classes have been annotated with a Spring Context Configuration.\n" +
190-
"Falling back to a generic application context.\n" +
191-
"This fallback has beep deprecated. Please annotate a glue class with some context configuration.\n" +
192-
"\n" +
193-
"For example:\n" +
194-
"\n" +
195-
" @@CucumberContextConfiguration\n" +
196-
" @SpringBootTest(classes = TestConfig.class)\n" +
197-
" public class CucumberSpringConfiguration { }" +
198-
"\n" +
199-
"Or: \n" +
200-
"\n" +
201-
" @@CucumberContextConfiguration\n" +
202-
" @ContextConfiguration( ... )\n" +
203-
" public class CucumberSpringConfiguration { }"
204-
);
205-
}
206122

207-
private void warnAboutDeprecationOfCucumberXml() {
208-
log.warn(() -> "" +
209-
"You are using cucumber.xml to configure the Spring Application Context.\n" +
210-
"cucumber.xml has been deprecated. Instead consider annotation based configuration.\n" +
211-
"You may create a glue class containing:\n" +
212-
"\n" +
213-
" @@CucumberContextConfiguration\n" +
214-
" @ContextConfiguration(\"classpath:cucumber.xml\")\n" +
215-
" public class CucumberSpringConfiguration { }"
216-
);
123+
// The application context created by the TestContextManager is
124+
// a singleton and reused between scenarios and shared between
125+
// threads.
126+
TestContextManager testContextManager = new TestContextManager(withCucumberContextConfiguration);
127+
testContextAdaptor = createTestContextManagerAdaptor(testContextManager, stepClasses);
128+
testContextAdaptor.start();
217129
}
218130

219-
220131
@Override
221132
public void stop() {
222133
if (testContextAdaptor != null) {
@@ -233,4 +144,29 @@ public <T> T getInstance(final Class<T> type) {
233144
}
234145
}
235146

236-
}
147+
private static boolean hasComponentAnnotation(Annotation annotation) {
148+
return hasAnnotation(annotation, Collections.singleton(Component.class));
149+
}
150+
151+
private static boolean hasAnnotation(Annotation annotation, Collection<Class<? extends Annotation>> desired) {
152+
Set<Class<? extends Annotation>> seen = new HashSet<>();
153+
Deque<Class<? extends Annotation>> toCheck = new ArrayDeque<>();
154+
toCheck.add(annotation.annotationType());
155+
156+
while (!toCheck.isEmpty()) {
157+
Class<? extends Annotation> annotationType = toCheck.pop();
158+
if (desired.contains(annotationType)) {
159+
return true;
160+
}
161+
162+
seen.add(annotationType);
163+
for (Annotation annotationTypesAnnotations : annotationType.getAnnotations()) {
164+
if (!seen.contains(annotationTypesAnnotations.annotationType())) {
165+
toCheck.add(annotationTypesAnnotations.annotationType());
166+
}
167+
}
168+
169+
}
170+
return false;
171+
}
172+
}

0 commit comments

Comments
 (0)