diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
new file mode 100644
index 0000000000..0e020683cc
--- /dev/null
+++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizer.java
@@ -0,0 +1,258 @@
+/*
+ * 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.Annotation;
+import java.lang.reflect.AnnotatedElement;
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+import org.springframework.beans.BeansException;
+import org.springframework.beans.factory.BeanFactory;
+import org.springframework.beans.factory.BeanFactoryAware;
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
+import org.springframework.beans.factory.support.BeanDefinitionRegistry;
+import org.springframework.beans.factory.support.BeanDefinitionRegistryPostProcessor;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
+import org.springframework.context.annotation.Configuration;
+import org.springframework.context.annotation.Import;
+import org.springframework.context.annotation.ImportSelector;
+import org.springframework.context.support.AbstractApplicationContext;
+import org.springframework.core.Ordered;
+import org.springframework.core.annotation.AnnotationUtils;
+import org.springframework.core.annotation.Order;
+import org.springframework.core.type.AnnotationMetadata;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.MergedContextConfiguration;
+
+/**
+ * {@link ContextCustomizer} to allow {@code @Import} annotations to be used directly on
+ * test classes.
+ *
+ * @author Phillip Webb
+ * @see ImportsContextCustomizerFactory
+ */
+class ImportsContextCustomizer implements ContextCustomizer {
+
+ static final String TEST_CLASS_ATTRIBUTE = "testClass";
+
+ private final Class> testClass;
+
+ private final ContextCustomizerKey key;
+
+ ImportsContextCustomizer(Class> testClass) {
+ this.testClass = testClass;
+ this.key = new ContextCustomizerKey(testClass);
+ }
+
+ @Override
+ public void customizeContext(ConfigurableApplicationContext context,
+ MergedContextConfiguration mergedContextConfiguration) {
+ BeanDefinitionRegistry registry = getBeanDefinitionRegistry(context);
+ AnnotatedBeanDefinitionReader reader = new AnnotatedBeanDefinitionReader(
+ registry);
+ registerCleanupPostProcessor(registry, reader);
+ registerImportsConfiguration(registry, reader);
+ }
+
+ private void registerCleanupPostProcessor(BeanDefinitionRegistry registry,
+ AnnotatedBeanDefinitionReader reader) {
+ BeanDefinition definition = registerBean(registry, reader,
+ ImportsCleanupPostProcessor.BEAN_NAME, ImportsCleanupPostProcessor.class);
+ definition.getConstructorArgumentValues().addIndexedArgumentValue(0,
+ this.testClass);
+ }
+
+ private void registerImportsConfiguration(BeanDefinitionRegistry registry,
+ AnnotatedBeanDefinitionReader reader) {
+ BeanDefinition definition = registerBean(registry, reader,
+ ImportsConfiguration.BEAN_NAME, ImportsConfiguration.class);
+ definition.setAttribute(TEST_CLASS_ATTRIBUTE, this.testClass);
+ }
+
+ private BeanDefinitionRegistry getBeanDefinitionRegistry(ApplicationContext context) {
+ if (context instanceof BeanDefinitionRegistry) {
+ return (BeanDefinitionRegistry) context;
+ }
+ if (context instanceof AbstractApplicationContext) {
+ return (BeanDefinitionRegistry) ((AbstractApplicationContext) context)
+ .getBeanFactory();
+ }
+ throw new IllegalStateException("Could not locate BeanDefinitionRegistry");
+ }
+
+ @SuppressWarnings("unchecked")
+ private BeanDefinition registerBean(BeanDefinitionRegistry registry,
+ AnnotatedBeanDefinitionReader reader, String beanName, Class> type) {
+ reader.registerBean(type, beanName);
+ BeanDefinition definition = registry.getBeanDefinition(beanName);
+ return definition;
+ }
+
+ @Override
+ public int hashCode() {
+ return this.key.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == this) {
+ return true;
+ }
+ if (obj == null || obj.getClass() != getClass()) {
+ return false;
+ }
+ // ImportSelectors are flexible so the only safe cache key is the test class
+ ImportsContextCustomizer other = (ImportsContextCustomizer) obj;
+ return this.key.equals(other.key);
+ }
+
+ /**
+ * {@link Configuration} registered to trigger the {@link ImportsSelector}.
+ */
+ @Configuration
+ @Import(ImportsSelector.class)
+ static class ImportsConfiguration {
+
+ static final String BEAN_NAME = ImportsConfiguration.class.getName();
+
+ }
+
+ /**
+ * {@link ImportSelector} that returns the original test class so that direct
+ * {@code @Import} annotations are processed.
+ */
+ static class ImportsSelector implements ImportSelector, BeanFactoryAware {
+
+ private static final String[] NO_IMPORTS = {};
+
+ private ConfigurableListableBeanFactory beanFactory;
+
+ @Override
+ public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
+ this.beanFactory = (ConfigurableListableBeanFactory) beanFactory;
+ }
+
+ @Override
+ public String[] selectImports(AnnotationMetadata importingClassMetadata) {
+ BeanDefinition definition = this.beanFactory
+ .getBeanDefinition(ImportsConfiguration.BEAN_NAME);
+ Object testClass = (definition == null ? null
+ : definition.getAttribute(TEST_CLASS_ATTRIBUTE));
+ return (testClass == null ? NO_IMPORTS
+ : new String[] { ((Class>) testClass).getName() });
+ }
+
+ }
+
+ /**
+ * {@link BeanDefinitionRegistryPostProcessor} to cleanup temporary configuration
+ * added to load imports.
+ */
+ @Order(Ordered.LOWEST_PRECEDENCE)
+ static class ImportsCleanupPostProcessor
+ implements BeanDefinitionRegistryPostProcessor {
+
+ static final String BEAN_NAME = ImportsCleanupPostProcessor.class.getName();
+
+ private final Class> testClass;
+
+ ImportsCleanupPostProcessor(Class> testClass) {
+ this.testClass = testClass;
+ }
+
+ @Override
+ public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)
+ throws BeansException {
+ }
+
+ @Override
+ public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry)
+ throws BeansException {
+ try {
+ String[] names = registry.getBeanDefinitionNames();
+ for (String name : names) {
+ BeanDefinition definition = registry.getBeanDefinition(name);
+ if (this.testClass.getName().equals(definition.getBeanClassName())) {
+ registry.removeBeanDefinition(name);
+ }
+ }
+ registry.removeBeanDefinition(ImportsConfiguration.BEAN_NAME);
+ }
+ catch (NoSuchBeanDefinitionException ex) {
+ }
+ }
+
+ }
+
+ /**
+ * The key used to ensure correct application context caching. Keys are generated
+ * based on all the annotations used with the test. We must use something
+ * broader than just {@link Import @Import} annotations since an {@code @Import} may
+ * use an {@link ImportSelector} which could make decisions based on anything
+ * available from {@link AnnotationMetadata}.
+ */
+ static class ContextCustomizerKey {
+
+ private final Set annotations;
+
+ ContextCustomizerKey(Class> testClass) {
+ Set annotations = new HashSet();
+ collectClassAnnotations(testClass, annotations);
+ this.annotations = Collections.unmodifiableSet(annotations);
+ }
+
+ private void collectClassAnnotations(Class> classType,
+ Set annotations) {
+ collectElementAnnotations(classType, annotations);
+ for (Class> interfaceType : classType.getInterfaces()) {
+ collectClassAnnotations(interfaceType, annotations);
+ }
+ if (classType.getSuperclass() != null) {
+ collectClassAnnotations(classType.getSuperclass(), annotations);
+ }
+ }
+
+ private void collectElementAnnotations(AnnotatedElement element,
+ Set annotations) {
+ for (Annotation annotation : element.getDeclaredAnnotations()) {
+ if (!AnnotationUtils.isInJavaLangAnnotationPackage(annotation)) {
+ annotations.add(annotation);
+ collectClassAnnotations(annotation.annotationType(), annotations);
+ }
+ }
+ }
+
+ @Override
+ public int hashCode() {
+ return this.annotations.hashCode();
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return (obj != null && getClass().equals(obj.getClass())
+ && this.annotations.equals(((ContextCustomizerKey) obj).annotations));
+ }
+
+ }
+
+}
diff --git a/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java
new file mode 100644
index 0000000000..c44951548c
--- /dev/null
+++ b/spring-boot-test/src/main/java/org/springframework/boot/test/context/ImportsContextCustomizerFactory.java
@@ -0,0 +1,64 @@
+/*
+ * 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.reflect.Method;
+import java.util.List;
+
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.core.annotation.AnnotatedElementUtils;
+import org.springframework.test.context.ContextConfigurationAttributes;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.ContextCustomizerFactory;
+import org.springframework.util.Assert;
+import org.springframework.util.ReflectionUtils;
+import org.springframework.util.ReflectionUtils.MethodCallback;
+
+/**
+ * {@link ContextCustomizerFactory} to allow {@code @Import} annotations to be used
+ * directly on test classes.
+ *
+ * @author Phillip Webb
+ * @see ImportsContextCustomizer
+ */
+class ImportsContextCustomizerFactory implements ContextCustomizerFactory {
+
+ @Override
+ public ContextCustomizer createContextCustomizer(Class> testClass,
+ List configAttributes) {
+ if (AnnotatedElementUtils.findMergedAnnotation(testClass, Import.class) != null) {
+ assertHasNoBeanMethods(testClass);
+ return new ImportsContextCustomizer(testClass);
+ }
+ return null;
+ }
+
+ private void assertHasNoBeanMethods(Class> testClass) {
+ ReflectionUtils.doWithMethods(testClass, new MethodCallback() {
+
+ @Override
+ public void doWith(Method method) {
+ Assert.state(!AnnotatedElementUtils.isAnnotated(method, Bean.class),
+ "Test classes cannot include @Bean methods");
+ }
+
+ });
+
+ }
+
+}
diff --git a/spring-boot-test/src/main/resources/META-INF/spring.factories b/spring-boot-test/src/main/resources/META-INF/spring.factories
index 50479f7341..3a72cef79f 100644
--- a/spring-boot-test/src/main/resources/META-INF/spring.factories
+++ b/spring-boot-test/src/main/resources/META-INF/spring.factories
@@ -1,5 +1,6 @@
# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
+org.springframework.boot.test.context.ImportsContextCustomizerFactory,\
org.springframework.boot.test.context.filter.ExcludeFilterContextCustomizerFactory,\
org.springframework.boot.test.context.web.WebIntegrationTestContextCustomizerFactory,\
org.springframework.boot.test.mock.mockito.MockitoContextCustomizerFactory
diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryIntegrationTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryIntegrationTests.java
new file mode 100644
index 0000000000..aff1b9c6b6
--- /dev/null
+++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryIntegrationTests.java
@@ -0,0 +1,63 @@
+/*
+ * 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 org.junit.Test;
+import org.junit.runner.RunWith;
+
+import org.springframework.beans.factory.NoSuchBeanDefinitionException;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.ImportsContextCustomizerFactoryIntegrationTests.ImportedBean;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.annotation.Import;
+import org.springframework.stereotype.Component;
+import org.springframework.test.context.junit4.SpringRunner;
+
+import static org.assertj.core.api.Assertions.assertThat;
+
+/**
+ * Integration tests for {@link ImportsContextCustomizerFactory} and
+ * {@link ImportsContextCustomizer}.
+ *
+ * @author Phillip Webb
+ */
+@RunWith(SpringRunner.class)
+@Import(ImportedBean.class)
+public class ImportsContextCustomizerFactoryIntegrationTests {
+
+ @Autowired
+ private ApplicationContext context;
+
+ @Autowired
+ private ImportedBean bean;
+
+ @Test
+ public void beanWasImported() throws Exception {
+ assertThat(this.bean).isNotNull();
+ }
+
+ @Test(expected = NoSuchBeanDefinitionException.class)
+ public void testItselfIsNotABean() throws Exception {
+ this.context.getBean(getClass());
+ }
+
+ @Component
+ static class ImportedBean {
+
+ }
+
+}
diff --git a/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java
new file mode 100644
index 0000000000..6c2f273413
--- /dev/null
+++ b/spring-boot-test/src/test/java/org/springframework/boot/test/context/ImportsContextCustomizerFactoryTests.java
@@ -0,0 +1,156 @@
+/*
+ * 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.Retention;
+import java.lang.annotation.RetentionPolicy;
+
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.rules.ExpectedException;
+
+import org.springframework.context.annotation.AnnotationConfigApplicationContext;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Import;
+import org.springframework.stereotype.Component;
+import org.springframework.test.context.ContextCustomizer;
+import org.springframework.test.context.MergedContextConfiguration;
+
+import static org.assertj.core.api.Assertions.assertThat;
+import static org.mockito.Mockito.mock;
+
+/**
+ * Tests for {@link ImportsContextCustomizerFactory} and {@link ImportsContextCustomizer}.
+ *
+ * @author Phillip Webb
+ */
+public class ImportsContextCustomizerFactoryTests {
+
+ @Rule
+ public ExpectedException thrown = ExpectedException.none();
+
+ private ImportsContextCustomizerFactory factory = new ImportsContextCustomizerFactory();
+
+ @Test
+ public void getContextCustomizerWhenHasNoImportAnnotationShouldReturnNull() {
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(TestWithNoImport.class, null);
+ assertThat(customizer).isNull();
+ }
+
+ @Test
+ public void getContextCustomizerWhenHasImportAnnotationShouldReturnCustomizer() {
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(TestWithImport.class, null);
+ assertThat(customizer).isNotNull();
+ }
+
+ @Test
+ public void getContextCustomizerWhenHasMetaImportAnnotationShouldReturnCustomizer() {
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(TestWithMetaImport.class, null);
+ assertThat(customizer).isNotNull();
+ }
+
+ @Test
+ public void contextCustomizerEqualsAndHashCode() throws Exception {
+ ContextCustomizer customizer1 = this.factory
+ .createContextCustomizer(TestWithImport.class, null);
+ ContextCustomizer customizer2 = this.factory
+ .createContextCustomizer(TestWithImport.class, null);
+ ContextCustomizer customizer3 = this.factory
+ .createContextCustomizer(TestWithImportAndMetaImport.class, null);
+ ContextCustomizer customizer4 = this.factory
+ .createContextCustomizer(TestWithSameImportAndMetaImport.class, null);
+ assertThat(customizer1.hashCode()).isEqualTo(customizer1.hashCode());
+ assertThat(customizer1.hashCode()).isEqualTo(customizer2.hashCode());
+ assertThat(customizer1).isEqualTo(customizer1).isEqualTo(customizer2)
+ .isNotEqualTo(customizer3);
+ assertThat(customizer3).isEqualTo(customizer4);
+ }
+
+ @Test
+ public void getContextCustomizerWhenClassHasBeanMethodsShouldThrowException()
+ throws Exception {
+ this.thrown.expect(IllegalStateException.class);
+ this.thrown.expectMessage("Test classes cannot include @Bean methods");
+ this.factory.createContextCustomizer(TestWithImportAndBeanMethod.class, null);
+ }
+
+ @Test
+ public void contextCustomizerImportsBeans() throws Exception {
+ ContextCustomizer customizer = this.factory
+ .createContextCustomizer(TestWithImport.class, null);
+ AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
+ customizer.customizeContext(context, mock(MergedContextConfiguration.class));
+ context.refresh();
+ assertThat(context.getBean(ImportedBean.class)).isNotNull();
+ }
+
+ static class TestWithNoImport {
+
+ }
+
+ @Import(ImportedBean.class)
+ static class TestWithImport {
+
+ }
+
+ @MetaImport
+ static class TestWithMetaImport {
+
+ }
+
+ @MetaImport
+ @Import(AnotherImportedBean.class)
+ static class TestWithImportAndMetaImport {
+
+ }
+
+ @MetaImport
+ @Import(AnotherImportedBean.class)
+ static class TestWithSameImportAndMetaImport {
+
+ }
+
+ @Import(ImportedBean.class)
+ static class TestWithImportAndBeanMethod {
+
+ @Bean
+ public String bean() {
+ return "bean";
+ }
+
+ }
+
+ @Retention(RetentionPolicy.RUNTIME)
+ @Import(ImportedBean.class)
+ @interface MetaImport {
+
+ }
+
+ @Component
+ static class ImportedBean {
+
+ }
+
+ @Component
+ static class AnotherImportedBean {
+
+ }
+
+}