From e85e9768c578fa1e5fbdb52a77115c36db8def07 Mon Sep 17 00:00:00 2001 From: Sam Brannen Date: Sun, 26 Apr 2015 02:58:07 +0200 Subject: [PATCH] Fail if multiple @BootstrapWith's are present Prior to this commit it was possible for two @BootstrapWith annotations to be 'present' on a test class -- for example, via competing custom composed annotations. However, only one of the annotations will ever be used to bootstrap the TestContext Framework. Thus, in such scenarios one of the annotations will be silently ignored. This commit introduces a check for such scenarios. BootstrapUtils' resolveTestContextBootstrapper() method now throws an IllegalStateException if more than one @BootstrapWith annotation is 'present' on a given test class. Issue: SPR-12602 --- .../test/context/BootstrapUtils.java | 28 +++- .../test/context/BootstrapUtilsTests.java | 127 ++++++++++++++++++ 2 files changed, 150 insertions(+), 5 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java index 6d37763835..411b3254dd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java @@ -17,14 +17,17 @@ package org.springframework.test.context; import java.lang.reflect.Constructor; +import java.util.List; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; import org.springframework.util.ClassUtils; +import org.springframework.util.MultiValueMap; import static org.springframework.beans.BeanUtils.*; -import static org.springframework.core.annotation.AnnotationUtils.*; /** * {@code BootstrapUtils} is a collection of utility methods to assist with @@ -120,9 +123,19 @@ abstract class BootstrapUtils { Class clazz = null; try { - BootstrapWith bootstrapWith = findAnnotation(testClass, BootstrapWith.class); - if (bootstrapWith != null && !TestContextBootstrapper.class.equals(bootstrapWith.value())) { - clazz = bootstrapWith.value(); + + MultiValueMap attributesMultiMap = AnnotatedElementUtils.getAllAnnotationAttributes( + testClass, BootstrapWith.class.getName()); + List values = (attributesMultiMap == null ? null : attributesMultiMap.get(AnnotationUtils.VALUE)); + + if (values != null) { + if (values.size() != 1) { + String msg = String.format( + "Configuration error: found multiple declarations of @BootstrapWith on test class [%s] with values %s", + testClass.getName(), values); + throw new IllegalStateException(msg); + } + clazz = (Class) values.get(0); } else { clazz = (Class) ClassUtils.forName( @@ -130,7 +143,8 @@ abstract class BootstrapUtils { } if (logger.isDebugEnabled()) { - logger.debug(String.format("Instantiating TestContextBootstrapper from class [%s]", clazz.getName())); + logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]", + testClass.getName(), clazz.getName())); } TestContextBootstrapper testContextBootstrapper = instantiateClass(clazz, TestContextBootstrapper.class); @@ -139,6 +153,10 @@ abstract class BootstrapUtils { return testContextBootstrapper; } catch (Throwable t) { + if (t instanceof IllegalStateException) { + throw (IllegalStateException) t; + } + throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz + "]. Specify @BootstrapWith's 'value' attribute " + "or make the default bootstrapper class available.", t); diff --git a/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java new file mode 100644 index 0000000000..8efb56d3ca --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/BootstrapUtilsTests.java @@ -0,0 +1,127 @@ +/* + * Copyright 2002-2015 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package org.springframework.test.context; + +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.test.context.support.DefaultTestContextBootstrapper; + +import static org.hamcrest.CoreMatchers.*; +import static org.junit.Assert.*; +import static org.mockito.Mockito.*; +import static org.springframework.test.context.BootstrapUtils.*; + +/** + * Unit tests for {@link BootstrapUtils}. + * + * @author Sam Brannen + * @since 4.2 + */ +public class BootstrapUtilsTests { + + private final CacheAwareContextLoaderDelegate delegate = mock(CacheAwareContextLoaderDelegate.class); + + @Rule + public ExpectedException exception = ExpectedException.none(); + + @Test + public void resolveTestContextBootstrapperForNonAnnotatedClass() { + assertBootstrapper(NonAnnotatedClass.class, DefaultTestContextBootstrapper.class); + } + + @Test + public void resolveTestContextBootstrapperWithEmptyBootstrapWithAnnotation() { + BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(EmptyBootstrapWithAnnotationClass.class, delegate); + + exception.expect(IllegalStateException.class); + exception.expectMessage(containsString("Specify @BootstrapWith's 'value' attribute")); + + resolveTestContextBootstrapper(bootstrapContext); + } + + @Test + public void resolveTestContextBootstrapperWithDirectBootstrapWithAnnotation() { + assertBootstrapper(DirectBootstrapWithAnnotationClass.class, FooBootstrapper.class); + } + + @Test + public void resolveTestContextBootstrapperWithInheritedBootstrapWithAnnotation() { + assertBootstrapper(InheritedBootstrapWithAnnotationClass.class, FooBootstrapper.class); + } + + @Test + public void resolveTestContextBootstrapperWithMetaBootstrapWithAnnotation() { + assertBootstrapper(MetaAnnotatedBootstrapWithAnnotationClass.class, BarBootstrapper.class); + } + + @Test + public void resolveTestContextBootstrapperWithDoubleMetaBootstrapWithAnnotation() { + BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext( + DoubleMetaAnnotatedBootstrapWithAnnotationClass.class, delegate); + + exception.expect(IllegalStateException.class); + exception.expectMessage(containsString("found multiple declarations of @BootstrapWith")); + exception.expectMessage(containsString(FooBootstrapper.class.getName())); + exception.expectMessage(containsString(BarBootstrapper.class.getName())); + + resolveTestContextBootstrapper(bootstrapContext); + } + + private void assertBootstrapper(Class testClass, Class expectedBootstrapper) { + BootstrapContext bootstrapContext = BootstrapTestUtils.buildBootstrapContext(testClass, delegate); + TestContextBootstrapper bootstrapper = resolveTestContextBootstrapper(bootstrapContext); + assertNotNull(bootstrapper); + assertEquals(expectedBootstrapper, bootstrapper.getClass()); + } + + // ------------------------------------------------------------------- + + static class FooBootstrapper extends DefaultTestContextBootstrapper {} + + static class BarBootstrapper extends DefaultTestContextBootstrapper {} + + @BootstrapWith(FooBootstrapper.class) + @Retention(RetentionPolicy.RUNTIME) + static @interface BootWithFoo {} + + @BootstrapWith(BarBootstrapper.class) + @Retention(RetentionPolicy.RUNTIME) + static @interface BootWithBar {} + + static class NonAnnotatedClass {} + + @BootstrapWith + static class EmptyBootstrapWithAnnotationClass {} + + @BootstrapWith(FooBootstrapper.class) + static class DirectBootstrapWithAnnotationClass {} + + static class InheritedBootstrapWithAnnotationClass extends DirectBootstrapWithAnnotationClass {} + + @BootWithBar + static class MetaAnnotatedBootstrapWithAnnotationClass {} + + @BootWithBar + @BootWithFoo + static class DoubleMetaAnnotatedBootstrapWithAnnotationClass {} + +}