Skip to content

Commit dba4f32

Browse files
committed
[Spring] Throw exception when step definitions are annotated with component
When step definitions are annotated with @component or other related annotations they can be picked up by springs class path scanning. This conflicts with cucumbers class path scanning and may result in multiple bean definitions for the same class. This problem is hard to understand and hard to trace. By making the problem explicit and providing a clear instruction on how to resolve this we can hopefully avoid future confusion. The current implementation only checks the a subset of all annotations. This will hopefully be sufficient. Closes #1225
1 parent 9d26ce5 commit dba4f32

File tree

6 files changed

+140
-7
lines changed

6 files changed

+140
-7
lines changed

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

+35-7
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package cucumber.runtime.java.spring;
22

3+
import static java.util.Arrays.asList;
34
import static org.springframework.test.context.FixBootstrapUtils.createBootstrapContext;
45
import static org.springframework.test.context.FixBootstrapUtils.resolveTestContextBootstrapper;
56

@@ -13,34 +14,44 @@
1314
import org.springframework.context.ConfigurableApplicationContext;
1415
import org.springframework.context.support.ClassPathXmlApplicationContext;
1516
import org.springframework.context.support.GenericApplicationContext;
17+
import org.springframework.stereotype.Component;
18+
import org.springframework.stereotype.Controller;
19+
import org.springframework.stereotype.Repository;
20+
import org.springframework.stereotype.Service;
1621
import org.springframework.test.context.ContextConfiguration;
1722
import org.springframework.test.context.ContextHierarchy;
1823
import org.springframework.test.context.TestContextManager;
1924

2025
import java.lang.annotation.Annotation;
2126
import java.util.Collection;
2227
import java.util.HashSet;
28+
import java.util.List;
2329

2430
/**
2531
* Spring based implementation of ObjectFactory.
2632
* <p/>
2733
* <p>
2834
* <ul>
2935
* <li>It uses TestContextManager to manage the spring context.
30-
* Configuration via: @ContextConfiguration or @ContextHierarcy
31-
* At least on step definition class needs to have a @ContextConfiguration or
32-
* @ContextHierarchy annotation. If more that one step definition class has such
36+
* Configuration via: @{@link ContextConfiguration} or @{@link ContextHierarchy}
37+
* At least one step definition class needs to have a @ContextConfiguration
38+
* or @ContextHierarchy annotation. If more that one step definition class has such
3339
* an annotation, the annotations must be equal on the different step definition
34-
* classes. If no step definition class with @ContextConfiguration or
35-
* @ContextHierarcy is found, it will try to load cucumber.xml from the classpath.
40+
* classes. If no step definition class with @ContextConfiguration or @ContextHierarchy
41+
* is found, it will try to load cucumber.xml from the classpath.
3642
* </li>
3743
* <li>The step definitions class with @ContextConfiguration or @ContextHierarchy
38-
* annotation, may also have a @WebAppConfiguration or @DirtiesContext annotation.
44+
* annotation, may also have a @{@link org.springframework.test.context.web.WebAppConfiguration}
45+
* or @{@link org.springframework.test.annotation.DirtiesContext} annotation.
3946
* </li>
4047
* <li>The step definitions added to the TestContextManagers context and
4148
* is reloaded for each scenario.</li>
49+
* <li>Step definition classes should not be annotated with @{@link Component} or
50+
* other annotations that marks it as eligible for detection by classpath scanning.</li>
51+
* <li>When a step definition class is annotated with @Component, @{@link Controller}, @{@link Repository}
52+
* or @{@link Service} an exception will be thrown.</li>
53+
* </li>
4254
* </ul>
43-
* </p>
4455
* <p/>
4556
* <p>
4657
* Application beans are accessible from the step definitions using autowiring
@@ -49,6 +60,8 @@
4960
*/
5061
public class SpringFactory implements ObjectFactory {
5162

63+
private static final List<Class<? extends Annotation>> componentStereoTypes = asList(Component.class, Controller.class, Repository.class, Service.class);
64+
5265
private ConfigurableListableBeanFactory beanFactory;
5366
private CucumberTestContextManager testContextManager;
5467

@@ -61,6 +74,7 @@ public SpringFactory() {
6174
@Override
6275
public boolean addClass(final Class<?> stepClass) {
6376
if (!stepClasses.contains(stepClass)) {
77+
checkNoComponentAnnotations(stepClass);
6478
if (dependsOnSpringContext(stepClass)) {
6579
if (stepClassWithSpringContext == null) {
6680
stepClassWithSpringContext = stepClass;
@@ -73,6 +87,20 @@ public boolean addClass(final Class<?> stepClass) {
7387
return true;
7488
}
7589

90+
private void checkNoComponentAnnotations(Class<?> type) {
91+
for (Class<? extends Annotation> stereoType : componentStereoTypes) {
92+
if (type.isAnnotationPresent(stereoType)) {
93+
throw new CucumberException(String.format("" +
94+
"Glue class %1$s was annotated with @%2$s; marking it as a candidate for auto-detection by " +
95+
"Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by " +
96+
"spring may lead to duplicate bean definitions. Please remove the @%2$s annotation",
97+
type.getName(),
98+
stereoType.getSimpleName()));
99+
}
100+
}
101+
}
102+
103+
76104
private void checkAnnotationsEqual(Class<?> stepClassWithSpringContext, Class<?> stepClass) {
77105
Annotation[] annotations1 = stepClassWithSpringContext.getAnnotations();
78106
Annotation[] annotations2 = stepClass.getAnnotations();

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

+16
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@
77
import cucumber.runtime.java.spring.commonglue.AutowiresThirdStepDef;
88
import cucumber.runtime.java.spring.commonglue.OneStepDef;
99
import cucumber.runtime.java.spring.commonglue.ThirdStepDef;
10+
import cucumber.runtime.java.spring.componentannotation.WithComponentAnnotation;
11+
import cucumber.runtime.java.spring.componentannotation.WithControllerAnnotation;
1012
import cucumber.runtime.java.spring.metaconfig.general.BellyMetaStepdefs;
1113
import cucumber.runtime.java.spring.contextconfig.BellyStepdefs;
1214
import cucumber.runtime.java.spring.contextconfig.WithSpringAnnotations;
@@ -247,4 +249,18 @@ public void shouldFailIfClassesWithDifferentSpringAnnotationsAreFound() {
247249
factory.addClass(WithContextHierarchyAnnotation.class);
248250
factory.addClass(WithDifferentContextHierarchyAnnotation.class);
249251
}
252+
253+
@Test(expected=CucumberException.class)
254+
public void shouldFailIfClassWithSpringComponentAnnotationsIsFound() {
255+
final ObjectFactory factory = new SpringFactory();
256+
factory.addClass(WithComponentAnnotation.class);
257+
258+
}
259+
260+
@Test(expected=CucumberException.class)
261+
public void shouldFailIfClassWithAnnotationAnnotatedWithSpringComponentAnnotationsIsFound() {
262+
final ObjectFactory factory = new SpringFactory();
263+
factory.addClass(WithControllerAnnotation.class);
264+
265+
}
250266
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
package cucumber.runtime.java.spring.componentannotation;
2+
3+
import cucumber.runtime.java.spring.beans.DummyComponent;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.test.context.ContextConfiguration;
7+
import org.springframework.test.context.ContextHierarchy;
8+
9+
@Component
10+
public class WithComponentAnnotation {
11+
12+
private boolean autowired;
13+
14+
@Autowired
15+
public void setAutowiredCollaborator(DummyComponent collaborator) {
16+
autowired = true;
17+
}
18+
19+
public boolean isAutowired() {
20+
return autowired;
21+
}
22+
23+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cucumber.runtime.java.spring.componentannotation;
2+
3+
import cucumber.runtime.java.spring.beans.DummyComponent;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.stereotype.Controller;
6+
import org.springframework.stereotype.Repository;
7+
8+
@Controller
9+
public class WithControllerAnnotation {
10+
11+
private boolean autowired;
12+
13+
@Autowired
14+
public void setAutowiredCollaborator(DummyComponent collaborator) {
15+
autowired = true;
16+
}
17+
18+
public boolean isAutowired() {
19+
return autowired;
20+
}
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cucumber.runtime.java.spring.componentannotation;
2+
3+
import cucumber.runtime.java.spring.beans.DummyComponent;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.stereotype.Repository;
6+
import org.springframework.stereotype.Service;
7+
8+
@Repository
9+
public class WithRepositoryAnnotation {
10+
11+
private boolean autowired;
12+
13+
@Autowired
14+
public void setAutowiredCollaborator(DummyComponent collaborator) {
15+
autowired = true;
16+
}
17+
18+
public boolean isAutowired() {
19+
return autowired;
20+
}
21+
22+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cucumber.runtime.java.spring.componentannotation;
2+
3+
import cucumber.runtime.java.spring.beans.DummyComponent;
4+
import org.springframework.beans.factory.annotation.Autowired;
5+
import org.springframework.stereotype.Component;
6+
import org.springframework.stereotype.Service;
7+
8+
@Service
9+
public class WithServiceAnnotation {
10+
11+
private boolean autowired;
12+
13+
@Autowired
14+
public void setAutowiredCollaborator(DummyComponent collaborator) {
15+
autowired = true;
16+
}
17+
18+
public boolean isAutowired() {
19+
return autowired;
20+
}
21+
22+
}

0 commit comments

Comments
 (0)