From 600a06af8339ced8d7376dc3ba3a849f06b65017 Mon Sep 17 00:00:00 2001 From: Phillip Webb Date: Tue, 1 Mar 2016 15:38:22 -0800 Subject: [PATCH] Auto-exclude test components from scanning Add TestTypeExcludeFilter which will automatically attempt to exclude test only configurations. All `@Configuration` annotated inner-classes of tests are automatically excluded. The `@TestConfiguration` annotation can be used to explicitly if a configuration needs explicit exclusion. See gh-5295 See gh-4901 --- .../boot/test/context/TestComponent.java | 54 ++++++++++++++ .../ExcludeFilterContextCustomizer.java | 55 ++++++++++++++ ...ExcludeFilterContextCustomizerFactory.java | 41 ++++++++++ .../context/filter/TestTypeExcludeFilter.java | 70 ++++++++++++++++++ .../test/context/filter/package-info.java | 20 +++++ .../main/resources/META-INF/spring.factories | 3 + .../test/context/filter/SampleConfig.java | 24 ++++++ .../test/context/filter/SampleTestConfig.java | 26 +++++++ .../filter/TestTypeExcludeFilterTests.java | 74 +++++++++++++++++++ 9 files changed, 367 insertions(+) create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/TestComponent.java create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizer.java create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizerFactory.java create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java create mode 100644 spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/package-info.java create mode 100644 spring-boot-test/src/main/resources/META-INF/spring.factories create mode 100644 spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleConfig.java create mode 100644 spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleTestConfig.java create mode 100644 spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilterTests.java diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/TestComponent.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/TestComponent.java new file mode 100644 index 00000000000..0e5c25be62d --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/TestComponent.java @@ -0,0 +1,54 @@ +/* + * Copyright 2012-2016 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.boot.test.context; + +import java.lang.annotation.Documented; +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.context.annotation.ComponentScan; +import org.springframework.stereotype.Component; + +/** + * {@link Component @Component} that can be used when a bean is intended only for tests, + * and should be excluded from Spring Boot's component scanning. + *

+ * Note that if you directly use {@link ComponentScan @ComponentScan} rather than relying + * on {@code @SpringBootApplication} you should ensure that a {@link TypeExcludeFilter} is + * declared as an {@link ComponentScan#excludeFilters() excludeFilter}. + * + * @author Phillip Webb + * @since 1.4.0 + * @see TypeExcludeFilter + * @see TestConfiguration + */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) +@Documented +@Component +public @interface TestComponent { + + /** + * The value may indicate a suggestion for a logical component name, to be turned into + * a Spring bean in case of an auto-detected component. + */ + String value() default ""; + +} diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizer.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizer.java new file mode 100644 index 00000000000..678bd3ef492 --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizer.java @@ -0,0 +1,55 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; + +/** + * {@link ContextCustomizer} to add the {@link TestTypeExcludeFilter} to the + * {@link ApplicationContext}. + * + * @author Phillip Webb + */ +class ExcludeFilterContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, + MergedContextConfiguration mergedContextConfiguration) { + context.getBeanFactory().registerSingleton(TestTypeExcludeFilter.class.getName(), + new TestTypeExcludeFilter()); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + + @Override + public boolean equals(Object obj) { + if (obj == this) { + return true; + } + if (obj == null || getClass() != obj.getClass()) { + return false; + } + return true; + } + +} diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizerFactory.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizerFactory.java new file mode 100644 index 00000000000..72ad2047b86 --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/ExcludeFilterContextCustomizerFactory.java @@ -0,0 +1,41 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import java.util.List; + +import org.springframework.context.ApplicationContext; +import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; + +/** + * {@link ContextCustomizerFactory} to add the {@link TestTypeExcludeFilter} to the + * {@link ApplicationContext}. + * + * @author Phillip Webb + * @see ExcludeFilterContextCustomizer + */ +class ExcludeFilterContextCustomizerFactory implements ContextCustomizerFactory { + + @Override + public ContextCustomizer createContextCustomizer(Class testClass, + List configAttributes) { + return new ExcludeFilterContextCustomizer(); + } + +} diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java new file mode 100644 index 00000000000..2a10438e378 --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilter.java @@ -0,0 +1,70 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import java.io.IOException; + +import org.springframework.boot.context.TypeExcludeFilter; +import org.springframework.boot.test.context.TestComponent; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; + +/** + * {@link TypeExcludeFilter} to exclude classes annotated with {@link TestComponent} as + * well as inner-classes of tests. + * + * @author Phillip Webb + */ +class TestTypeExcludeFilter extends TypeExcludeFilter { + + private static final String TEST_ANNOTATION = "org.junit.Test"; + + @Override + public boolean match(MetadataReader metadataReader, + MetadataReaderFactory metadataReaderFactory) throws IOException { + if (isTestConfiguration(metadataReader)) { + return true; + } + if (isTestClass(metadataReader)) { + return true; + } + String enclosing = metadataReader.getClassMetadata().getEnclosingClassName(); + if (enclosing != null) { + try { + if (match(metadataReaderFactory.getMetadataReader(enclosing), + metadataReaderFactory)) { + return true; + } + } + catch (Exception ex) { + // Ignore + } + } + return false; + } + + private boolean isTestConfiguration(MetadataReader metadataReader) { + return (metadataReader.getAnnotationMetadata() + .isAnnotated(TestComponent.class.getName())); + } + + private boolean isTestClass(MetadataReader metadataReader) { + return !metadataReader.getAnnotationMetadata() + .getAnnotatedMethods(TEST_ANNOTATION).isEmpty(); + } + +} diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/package-info.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/package-info.java new file mode 100644 index 00000000000..80bbd7fc13f --- /dev/null +++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/filter/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2016 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. + */ + +/** + * Test support for {@link org.springframework.boot.context.TypeExcludeFilter}. + */ +package org.springframework.boot.test.context.filter; diff --git a/spring-boot-test/src/main/resources/META-INF/spring.factories b/spring-boot-test/src/main/resources/META-INF/spring.factories new file mode 100644 index 00000000000..08e4fa5ad5e --- /dev/null +++ b/spring-boot-test/src/main/resources/META-INF/spring.factories @@ -0,0 +1,3 @@ +# Spring Test ContextCustomizerFactories +org.springframework.test.context.ContextCustomizerFactory=\ +org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleConfig.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleConfig.java new file mode 100644 index 00000000000..d56e2849d3a --- /dev/null +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleConfig.java @@ -0,0 +1,24 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import org.springframework.context.annotation.Configuration; + +@Configuration +public class SampleConfig { + +} diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleTestConfig.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleTestConfig.java new file mode 100644 index 00000000000..0be1f7bf60f --- /dev/null +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/SampleTestConfig.java @@ -0,0 +1,26 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import org.springframework.boot.test.context.TestComponent; +import org.springframework.context.annotation.Configuration; + +@Configuration +@TestComponent +public class SampleTestConfig { + +} diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilterTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilterTests.java new file mode 100644 index 00000000000..ee1b6855764 --- /dev/null +++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/filter/TestTypeExcludeFilterTests.java @@ -0,0 +1,74 @@ +/* + * Copyright 2012-2016 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.boot.test.context.filter; + +import java.io.IOException; + +import org.junit.Test; + +import org.springframework.context.annotation.Configuration; +import org.springframework.core.type.classreading.MetadataReader; +import org.springframework.core.type.classreading.MetadataReaderFactory; +import org.springframework.core.type.classreading.SimpleMetadataReaderFactory; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link TestTypeExcludeFilter}. + * + * @author Phillip Webb + */ +public class TestTypeExcludeFilterTests { + + private TestTypeExcludeFilter filter = new TestTypeExcludeFilter(); + + private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory(); + + @Test + public void matchesTestClass() throws Exception { + assertThat(this.filter.match(getMetadataReader(TestTypeExcludeFilterTests.class), + this.metadataReaderFactory)).isTrue(); + } + + @Test + public void matchesNestedConfiguration() throws Exception { + assertThat(this.filter.match(getMetadataReader(NestedConfig.class), + this.metadataReaderFactory)).isTrue(); + } + + @Test + public void matchesTestConfiguration() throws Exception { + assertThat(this.filter.match(getMetadataReader(SampleTestConfig.class), + this.metadataReaderFactory)).isTrue(); + } + + @Test + public void doesNotMatchRegularConfiguration() throws Exception { + assertThat(this.filter.match(getMetadataReader(SampleConfig.class), + this.metadataReaderFactory)).isFalse(); + } + + private MetadataReader getMetadataReader(Class source) throws IOException { + return this.metadataReaderFactory.getMetadataReader(source.getName()); + } + + @Configuration + static class NestedConfig { + + } + +}