Skip to content

Commit db8eeaa

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 aeec6ea commit db8eeaa

File tree

17 files changed

+152
-42
lines changed

17 files changed

+152
-42
lines changed

Diff for: core/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -41,7 +41,7 @@
4141
</dependency>
4242
<dependency>
4343
<groupId>org.mockito</groupId>
44-
<artifactId>mockito-all</artifactId>
44+
<artifactId>mockito-core</artifactId>
4545
<scope>test</scope>
4646
</dependency>
4747
<dependency>

Diff for: gosu/pom.xml

-5
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,5 @@
5353
<artifactId>junit</artifactId>
5454
<scope>test</scope>
5555
</dependency>
56-
<dependency>
57-
<groupId>org.mockito</groupId>
58-
<artifactId>mockito-all</artifactId>
59-
<scope>test</scope>
60-
</dependency>
6156
</dependencies>
6257
</project>

Diff for: groovy/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@
4646
</dependency>
4747
<dependency>
4848
<groupId>org.mockito</groupId>
49-
<artifactId>mockito-all</artifactId>
49+
<artifactId>mockito-core</artifactId>
5050
<scope>test</scope>
5151
</dependency>
5252
</dependencies>

Diff for: guice/pom.xml

-5
Original file line numberDiff line numberDiff line change
@@ -42,10 +42,5 @@
4242
<artifactId>junit</artifactId>
4343
<scope>test</scope>
4444
</dependency>
45-
<dependency>
46-
<groupId>org.mockito</groupId>
47-
<artifactId>mockito-all</artifactId>
48-
<scope>test</scope>
49-
</dependency>
5045
</dependencies>
5146
</project>

Diff for: java/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
</dependency>
3030
<dependency>
3131
<groupId>org.mockito</groupId>
32-
<artifactId>mockito-all</artifactId>
32+
<artifactId>mockito-core</artifactId>
3333
<scope>test</scope>
3434
</dependency>
3535
</dependencies>

Diff for: junit/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333

3434
<dependency>
3535
<groupId>org.mockito</groupId>
36-
<artifactId>mockito-all</artifactId>
36+
<artifactId>mockito-core</artifactId>
3737
<scope>test</scope>
3838
</dependency>
3939
</dependencies>

Diff for: jython/pom.xml

-5
Original file line numberDiff line numberDiff line change
@@ -35,11 +35,6 @@
3535
<artifactId>cucumber-junit</artifactId>
3636
<scope>test</scope>
3737
</dependency>
38-
<dependency>
39-
<groupId>org.mockito</groupId>
40-
<artifactId>mockito-all</artifactId>
41-
<scope>test</scope>
42-
</dependency>
4338
<dependency>
4439
<groupId>junit</groupId>
4540
<artifactId>junit</artifactId>

Diff for: needle/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,7 @@
4242
</dependency>
4343
<dependency>
4444
<groupId>org.mockito</groupId>
45-
<artifactId>mockito-all</artifactId>
45+
<artifactId>mockito-core</artifactId>
4646
<scope>test</scope>
4747
</dependency>
4848
<dependency>

Diff for: osgi/pom.xml

-5
Original file line numberDiff line numberDiff line change
@@ -37,11 +37,6 @@
3737
<artifactId>junit</artifactId>
3838
<scope>test</scope>
3939
</dependency>
40-
<dependency>
41-
<groupId>org.mockito</groupId>
42-
<artifactId>mockito-all</artifactId>
43-
<scope>test</scope>
44-
</dependency>
4540
<dependency>
4641
<groupId>org.osgi</groupId>
4742
<artifactId>org.osgi.core</artifactId>

Diff for: pom.xml

+12-5
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
<!-- TestNG 6.11 makes the tests of the testng module to fail -->
4545
<testng.version>6.10</testng.version>
4646
<junit.version>4.12</junit.version>
47+
<hamcrest.version>1.3</hamcrest.version>
4748
<jython.version>2.7.1</jython.version>
4849
<mockito.version>1.10.19</mockito.version>
4950
<selenium.version>3.5.2</selenium.version>
@@ -349,6 +350,11 @@
349350
<artifactId>junit</artifactId>
350351
<version>${junit.version}</version>
351352
</dependency>
353+
<dependency>
354+
<groupId>org.hamcrest</groupId>
355+
<artifactId>hamcrest-core</artifactId>
356+
<version>${hamcrest.version}</version>
357+
</dependency>
352358
<dependency>
353359
<groupId>org.testng</groupId>
354360
<artifactId>testng</artifactId>
@@ -359,15 +365,16 @@
359365
<artifactId>jython-standalone</artifactId>
360366
<version>${jython.version}</version>
361367
</dependency>
362-
<dependency>
363-
<groupId>org.mockito</groupId>
364-
<artifactId>mockito-all</artifactId>
365-
<version>${mockito.version}</version>
366-
</dependency>
367368
<dependency>
368369
<groupId>org.mockito</groupId>
369370
<artifactId>mockito-core</artifactId>
370371
<version>${mockito.version}</version>
372+
<exclusions>
373+
<exclusion>
374+
<groupId>org.hamcrest</groupId>
375+
<artifactId>hamcrest-core</artifactId>
376+
</exclusion>
377+
</exclusions>
371378
</dependency>
372379
<dependency>
373380
<groupId>org.jsoup</groupId>

Diff for: rhino/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@
4444
</dependency>
4545
<dependency>
4646
<groupId>org.mockito</groupId>
47-
<artifactId>mockito-all</artifactId>
47+
<artifactId>mockito-core</artifactId>
4848
<scope>test</scope>
4949
</dependency>
5050
</dependencies>

Diff for: spring/pom.xml

+1-1
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@
6969
</dependency>
7070
<dependency>
7171
<groupId>org.mockito</groupId>
72-
<artifactId>mockito-all</artifactId>
72+
<artifactId>mockito-core</artifactId>
7373
<scope>test</scope>
7474
</dependency>
7575
<dependency>

Diff for: spring/src/main/java/cucumber/runtime/java/spring/SpringFactory.java

+53-8
Original file line numberDiff line numberDiff line change
@@ -13,34 +13,42 @@
1313
import org.springframework.context.ConfigurableApplicationContext;
1414
import org.springframework.context.support.ClassPathXmlApplicationContext;
1515
import org.springframework.context.support.GenericApplicationContext;
16+
import org.springframework.stereotype.Component;
1617
import org.springframework.test.context.ContextConfiguration;
1718
import org.springframework.test.context.ContextHierarchy;
1819
import org.springframework.test.context.TestContextManager;
1920

2021
import java.lang.annotation.Annotation;
2122
import java.util.Collection;
2223
import java.util.HashSet;
24+
import java.util.Set;
25+
import java.util.Stack;
2326

2427
/**
2528
* Spring based implementation of ObjectFactory.
2629
* <p/>
2730
* <p>
2831
* <ul>
2932
* <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
33+
* Configuration via: @{@link ContextConfiguration} or @{@link ContextHierarchy}
34+
* At least one step definition class needs to have a @ContextConfiguration
35+
* or @ContextHierarchy annotation. If more that one step definition class has such
3336
* 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.
37+
* classes. If no step definition class with @ContextConfiguration or @ContextHierarchy
38+
* is found, it will try to load cucumber.xml from the classpath.
3639
* </li>
3740
* <li>The step definitions class with @ContextConfiguration or @ContextHierarchy
38-
* annotation, may also have a @WebAppConfiguration or @DirtiesContext annotation.
41+
* annotation, may also have a @{@link org.springframework.test.context.web.WebAppConfiguration}
42+
* or @{@link org.springframework.test.annotation.DirtiesContext} annotation.
3943
* </li>
40-
* <li>The step definitions added to the TestContextManagers context and
44+
* <li>The step definitions are added to the TestContextManagers context and
4145
* is reloaded for each scenario.</li>
46+
* <li>Step definitions should not be annotated with @{@link Component} or
47+
* other annotations that mark it as eligible for detection by classpath scanning.</li>
48+
* <li>When a step definition class is annotated by @Component or an annotation that has the @Component stereotype an
49+
* exception will be thrown</li>
50+
* </li>
4251
* </ul>
43-
* </p>
4452
* <p/>
4553
* <p>
4654
* Application beans are accessible from the step definitions using autowiring
@@ -61,6 +69,7 @@ public SpringFactory() {
6169
@Override
6270
public boolean addClass(final Class<?> stepClass) {
6371
if (!stepClasses.contains(stepClass)) {
72+
checkNoComponentAnnotations(stepClass);
6473
if (dependsOnSpringContext(stepClass)) {
6574
if (stepClassWithSpringContext == null) {
6675
stepClassWithSpringContext = stepClass;
@@ -73,6 +82,42 @@ public boolean addClass(final Class<?> stepClass) {
7382
return true;
7483
}
7584

85+
private static void checkNoComponentAnnotations(Class<?> type) {
86+
for (Annotation annotation : type.getAnnotations()) {
87+
if (hasComponentStereoType(annotation)) {
88+
throw new CucumberException(String.format("" +
89+
"Glue class %1$s was annotated with @%2$s; marking it as a candidate for auto-detection by " +
90+
"Spring. Glue classes are detected and registered by Cucumber. Auto-detection of glue classes by " +
91+
"spring may lead to duplicate bean definitions. Please remove the @%2$s annotation",
92+
type.getName(),
93+
annotation.annotationType().getSimpleName()));
94+
}
95+
}
96+
}
97+
98+
private static boolean hasComponentStereoType(Annotation annotation) {
99+
Set<Class<? extends Annotation>> seen = new HashSet<Class<? extends Annotation>>();
100+
Stack<Class<? extends Annotation>> toCheck = new Stack<Class<? extends Annotation>>();
101+
toCheck.add(annotation.annotationType());
102+
103+
while (!toCheck.isEmpty()) {
104+
Class<? extends Annotation> annotationType = toCheck.pop();
105+
if (Component.class.equals(annotationType)) {
106+
return true;
107+
}
108+
109+
seen.add(annotationType);
110+
for (Annotation annotationTypesAnnotations : annotationType.getAnnotations()) {
111+
if (!seen.contains(annotationTypesAnnotations.annotationType())) {
112+
toCheck.add(annotationTypesAnnotations.annotationType());
113+
}
114+
}
115+
116+
}
117+
return false;
118+
}
119+
120+
76121
private void checkAnnotationsEqual(Class<?> stepClassWithSpringContext, Class<?> stepClass) {
77122
Annotation[] annotations1 = stepClassWithSpringContext.getAnnotations();
78123
Annotation[] annotations2 = stepClass.getAnnotations();

Diff for: spring/src/test/java/cucumber/runtime/java/spring/SpringFactoryTest.java

+29-1
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,18 @@
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;
1315
import cucumber.runtime.java.spring.contexthierarchyconfig.WithContextHierarchyAnnotation;
1416
import cucumber.runtime.java.spring.contexthierarchyconfig.WithDifferentContextHierarchyAnnotation;
1517
import cucumber.runtime.java.spring.dirtiescontextconfig.DirtiesContextBellyStepDefs;
1618
import cucumber.runtime.java.spring.metaconfig.dirties.DirtiesContextBellyMetaStepDefs;
19+
import org.junit.Rule;
1720
import org.junit.Test;
21+
import org.junit.rules.ExpectedException;
1822
import org.springframework.transaction.PlatformTransactionManager;
1923

2024
import static org.junit.Assert.assertEquals;
@@ -25,6 +29,9 @@
2529

2630
public class SpringFactoryTest {
2731

32+
@Rule
33+
public ExpectedException expectedException = ExpectedException.none();
34+
2835
@Test
2936
public void shouldGiveUsNewStepInstancesForEachScenario() {
3037
final ObjectFactory factory = new SpringFactory();
@@ -241,10 +248,31 @@ public void shouldAllowClassesWithSameSpringAnnotations() {
241248
factory.addClass(BellyStepdefs.class);
242249
}
243250

244-
@Test(expected=CucumberException.class)
251+
@Test
245252
public void shouldFailIfClassesWithDifferentSpringAnnotationsAreFound() {
253+
expectedException.expect(CucumberException.class);
254+
expectedException.expectMessage("Annotations differs on glue classes found: cucumber.runtime.java.spring.contexthierarchyconfig.WithContextHierarchyAnnotation, cucumber.runtime.java.spring.contexthierarchyconfig.WithDifferentContextHierarchyAnnotation");
246255
final ObjectFactory factory = new SpringFactory();
247256
factory.addClass(WithContextHierarchyAnnotation.class);
248257
factory.addClass(WithDifferentContextHierarchyAnnotation.class);
249258
}
259+
260+
@Test
261+
public void shouldFailIfClassWithSpringComponentAnnotationsIsFound() {
262+
expectedException.expect(CucumberException.class);
263+
expectedException.expectMessage("Glue class cucumber.runtime.java.spring.componentannotation.WithComponentAnnotation was annotated with @Component");
264+
expectedException.expectMessage("Please remove the @Component annotation");
265+
final ObjectFactory factory = new SpringFactory();
266+
factory.addClass(WithComponentAnnotation.class);
267+
}
268+
269+
@Test
270+
public void shouldFailIfClassWithAnnotationAnnotatedWithSpringComponentAnnotationsIsFound() {
271+
expectedException.expect(CucumberException.class);
272+
expectedException.expectMessage("Glue class cucumber.runtime.java.spring.componentannotation.WithControllerAnnotation was annotated with @Controller");
273+
expectedException.expectMessage("Please remove the @Controller annotation");
274+
final ObjectFactory factory = new SpringFactory();
275+
factory.addClass(WithControllerAnnotation.class);
276+
277+
}
250278
}
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+
}

Diff for: testng/pom.xml

+6-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,12 @@
2222
</dependency>
2323
<dependency>
2424
<groupId>org.mockito</groupId>
25-
<artifactId>mockito-all</artifactId>
25+
<artifactId>mockito-core</artifactId>
26+
<scope>test</scope>
27+
</dependency>
28+
<dependency>
29+
<groupId>org.hamcrest</groupId>
30+
<artifactId>hamcrest-core</artifactId>
2631
<scope>test</scope>
2732
</dependency>
2833
</dependencies>

0 commit comments

Comments
 (0)