Introduce support for JUnit 5 in the TestContext framework

This commit introduces initial support for JUnit Jupiter (i.e., the new
programming and extension models in JUnit 5) in the Spring TestContext
Framework.

Specifically, this commit introduces the following.

- SpringExtension: an implementation of multiple extension APIs from
  JUnit Jupiter that provides full support for the existing feature set
  of the Spring TestContext Framework. This support is enabled via
  @ExtendWith(SpringExtension.class).

- @SpringJUnitConfig: a composed annotation that combines
  @ExtendWith(SpringExtension.class) from JUnit Jupiter with
  @ContextConfiguration from the Spring TestContext Framework.

- @SpringJUnitWebConfig: a composed annotation that combines
  @ExtendWith(SpringExtension.class) from JUnit Jupiter with
  @ContextConfiguration and @WebAppConfiguration from the Spring
  TestContext Framework.

Issue: SPR-13575
This commit is contained in:
Sam Brannen
2016-07-04 21:39:09 +02:00
parent 54e3ea8d37
commit 873fc53a2f
29 changed files with 1812 additions and 1 deletions

View File

@@ -0,0 +1,180 @@
/*
* Copyright 2002-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.test.context.junit.jupiter;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import org.junit.jupiter.api.extension.AfterAllCallback;
import org.junit.jupiter.api.extension.AfterEachCallback;
import org.junit.jupiter.api.extension.BeforeAllCallback;
import org.junit.jupiter.api.extension.BeforeEachCallback;
import org.junit.jupiter.api.extension.ContainerExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ExtensionContext.Namespace;
import org.junit.jupiter.api.extension.ExtensionContext.Store;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.jupiter.api.extension.TestExtensionContext;
import org.junit.jupiter.api.extension.TestInstancePostProcessor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.core.annotation.AnnotatedElementUtils;
import org.springframework.test.context.TestContextManager;
import org.springframework.test.context.junit.jupiter.support.ParameterAutowireUtils;
import org.springframework.util.Assert;
/**
* {@code SpringExtension} integrates the <em>Spring TestContext Framework</em>
* into JUnit 5's <em>Jupiter</em> programming model.
*
* <p>To use this class, simply annotate a JUnit Jupiter based test class with
* {@code @ExtendWith(SpringExtension.class)}.
*
* @author Sam Brannen
* @since 5.0
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
* @see org.springframework.test.context.TestContextManager
*/
public class SpringExtension implements BeforeAllCallback, AfterAllCallback, TestInstancePostProcessor,
BeforeEachCallback, AfterEachCallback, ParameterResolver {
/**
* {@link Namespace} in which {@code TestContextManagers} are stored, keyed
* by test class.
*/
private static final Namespace namespace = Namespace.create(SpringExtension.class);
/**
* Delegates to {@link TestContextManager#beforeTestClass}.
*/
@Override
public void beforeAll(ContainerExtensionContext context) throws Exception {
getTestContextManager(context).beforeTestClass();
}
/**
* Delegates to {@link TestContextManager#afterTestClass}.
*/
@Override
public void afterAll(ContainerExtensionContext context) throws Exception {
try {
getTestContextManager(context).afterTestClass();
}
finally {
context.getStore(namespace).remove(context.getTestClass().get());
}
}
/**
* Delegates to {@link TestContextManager#prepareTestInstance}.
*/
@Override
public void postProcessTestInstance(Object testInstance, ExtensionContext context) throws Exception {
getTestContextManager(context).prepareTestInstance(testInstance);
}
/**
* Delegates to {@link TestContextManager#beforeTestMethod}.
*/
@Override
public void beforeEach(TestExtensionContext context) throws Exception {
Object testInstance = context.getTestInstance();
Method testMethod = context.getTestMethod().get();
getTestContextManager(context).beforeTestMethod(testInstance, testMethod);
}
/**
* Delegates to {@link TestContextManager#afterTestMethod}.
*/
@Override
public void afterEach(TestExtensionContext context) throws Exception {
Object testInstance = context.getTestInstance();
Method testMethod = context.getTestMethod().get();
Throwable testException = context.getTestException().orElse(null);
getTestContextManager(context).afterTestMethod(testInstance, testMethod, testException);
}
/**
* Determine if the value for the {@link Parameter} in the supplied
* {@link ParameterContext} should be autowired from the test's
* {@link ApplicationContext}.
* <p>Returns {@code true} if the parameter is declared in a {@link Constructor}
* that is annotated with {@link Autowired @Autowired} and otherwise delegates
* to {@link ParameterAutowireUtils#isAutowirable}.
* <p><strong>WARNING</strong>: if the parameter is declared in a {@code Constructor}
* that is annotated with {@code @Autowired}, Spring will assume the responsibility
* for resolving all parameters in the constructor. Consequently, no other
* registered {@link ParameterResolver} will be able to resolve parameters.
*
* @see #resolve
* @see ParameterAutowireUtils#isAutowirable
*/
@Override
public boolean supports(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
Executable executable = parameter.getDeclaringExecutable();
return (executable instanceof Constructor && AnnotatedElementUtils.hasAnnotation(executable, Autowired.class))
|| ParameterAutowireUtils.isAutowirable(parameter);
}
/**
* Resolve a value for the {@link Parameter} in the supplied
* {@link ParameterContext} by retrieving the corresponding dependency
* from the test's {@link ApplicationContext}.
* <p>Delegates to {@link ParameterAutowireUtils#resolveDependency}.
* @see #supports
* @see ParameterAutowireUtils#resolveDependency
*/
@Override
public Object resolve(ParameterContext parameterContext, ExtensionContext extensionContext) {
Parameter parameter = parameterContext.getParameter();
Class<?> testClass = extensionContext.getTestClass().get();
ApplicationContext applicationContext = getApplicationContext(extensionContext);
return ParameterAutowireUtils.resolveDependency(parameter, testClass, applicationContext);
}
/**
* Get the {@link ApplicationContext} associated with the supplied
* {@code ExtensionContext}.
* @param context the current {@code ExtensionContext}; never {@code null}
* @return the application context
* @throws IllegalStateException if an error occurs while retrieving the
* application context
* @see org.springframework.test.context.TestContext#getApplicationContext()
*/
private ApplicationContext getApplicationContext(ExtensionContext context) {
return getTestContextManager(context).getTestContext().getApplicationContext();
}
/**
* Get the {@link TestContextManager} associated with the supplied
* {@code ExtensionContext}.
* @return the {@code TestContextManager}; never {@code null}
*/
private TestContextManager getTestContextManager(ExtensionContext context) {
Assert.notNull(context, "ExtensionContext must not be null");
Class<?> testClass = context.getTestClass().get();
Store store = context.getStore(namespace);
return store.getOrComputeIfAbsent(testClass, TestContextManager::new, TestContextManager.class);
}
}

View File

@@ -0,0 +1,96 @@
/*
* Copyright 2002-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.test.context.junit.jupiter;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.ContextConfiguration;
/**
* {@code @SpringJUnitConfig} is a <em>composed annotation</em> that combines
* {@link ExtendWith @ExtendWith(SpringExtension.class)} from JUnit Jupiter with
* {@link ContextConfiguration @ContextConfiguration} from the <em>Spring TestContext
* Framework</em>.
*
* @author Sam Brannen
* @since 5.0
* @see ExtendWith
* @see SpringExtension
* @see ContextConfiguration
* @see org.springframework.test.context.junit.jupiter.web.SpringJUnitWebConfig
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SpringJUnitConfig {
/**
* Alias for {@link ContextConfiguration#classes}.
*/
@AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
Class<?>[] value() default {};
/**
* Alias for {@link ContextConfiguration#classes}.
*/
@AliasFor(annotation = ContextConfiguration.class)
Class<?>[] classes() default {};
/**
* Alias for {@link ContextConfiguration#locations}.
*/
@AliasFor(annotation = ContextConfiguration.class)
String[] locations() default {};
/**
* Alias for {@link ContextConfiguration#initializers}.
*/
@AliasFor(annotation = ContextConfiguration.class)
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers() default {};
/**
* Alias for {@link ContextConfiguration#inheritLocations}.
*/
@AliasFor(annotation = ContextConfiguration.class)
boolean inheritLocations() default true;
/**
* Alias for {@link ContextConfiguration#inheritInitializers}.
*/
@AliasFor(annotation = ContextConfiguration.class)
boolean inheritInitializers() default true;
/**
* Alias for {@link ContextConfiguration#name}.
*/
@AliasFor(annotation = ContextConfiguration.class)
String name() default "";
}

View File

@@ -0,0 +1,5 @@
/**
* Core support for integrating the <em>Spring TestContext Framework</em>
* with the JUnit Jupiter extension model in JUnit 5.
*/
package org.springframework.test.context.junit.jupiter;

View File

@@ -0,0 +1,98 @@
/*
* Copyright 2002-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.test.context.junit.jupiter.support;
import java.lang.reflect.Constructor;
import java.lang.reflect.Executable;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.SynthesizingMethodParameter;
import org.springframework.util.Assert;
/**
* Factory for creating {@link MethodParameter} instances from Java 8
* {@link Parameter Parameters}.
*
* @author Sam Brannen
* @since 5.0
* @see ParameterAutowireUtils
* @see MethodParameter
* @see SynthesizingMethodParameter
* @see #createMethodParameter(Parameter)
* @see #createSynthesizingMethodParameter(Parameter)
*/
public abstract class MethodParameterFactory {
private MethodParameterFactory() {
/* no-op */
}
/**
* Create a standard {@link MethodParameter} from the supplied {@link Parameter}.
* <p>Supports parameters declared in methods and constructors.
* @param parameter the parameter to create a {@code MethodParameter} for;
* never {@code null}
* @return a new {@code MethodParameter}
* @see #createSynthesizingMethodParameter(Parameter)
*/
public static MethodParameter createMethodParameter(Parameter parameter) {
Assert.notNull(parameter, "Parameter must not be null");
Executable executable = parameter.getDeclaringExecutable();
if (executable instanceof Method) {
return new MethodParameter((Method) executable, getIndex(parameter));
}
// else
return new MethodParameter((Constructor<?>) executable, getIndex(parameter));
}
/**
* Create a {@link SynthesizingMethodParameter} from the supplied {@link Parameter}.
* <p>Supports parameters declared in methods.
* @param parameter the parameter to create a {@code SynthesizingMethodParameter}
* for; never {@code null}
* @return a new {@code SynthesizingMethodParameter}
* @throws UnsupportedOperationException if the supplied parameter is declared
* in a constructor
* @see #createMethodParameter(Parameter)
*/
public static SynthesizingMethodParameter createSynthesizingMethodParameter(Parameter parameter) {
Assert.notNull(parameter, "Parameter must not be null");
Executable executable = parameter.getDeclaringExecutable();
if (executable instanceof Method) {
return new SynthesizingMethodParameter((Method) executable, getIndex(parameter));
}
// else
throw new UnsupportedOperationException(
"Cannot create a SynthesizingMethodParameter for a constructor parameter: " + parameter);
}
private static int getIndex(Parameter parameter) {
Assert.notNull(parameter, "Parameter must not be null");
Executable executable = parameter.getDeclaringExecutable();
Parameter[] parameters = executable.getParameters();
for (int i = 0; i < parameters.length; i++) {
if (parameters[i] == parameter) {
return i;
}
}
throw new IllegalStateException(String.format("Failed to resolve index of parameter [%s] in executable [%s]",
parameter, executable.toGenericString()));
}
}

View File

@@ -0,0 +1,113 @@
/*
* Copyright 2002-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.test.context.junit.jupiter.support;
import java.lang.annotation.Annotation;
import java.lang.reflect.AnnotatedElement;
import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.util.Optional;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.beans.factory.config.AutowireCapableBeanFactory;
import org.springframework.beans.factory.config.DependencyDescriptor;
import org.springframework.context.ApplicationContext;
import org.springframework.core.MethodParameter;
import org.springframework.core.annotation.AnnotatedElementUtils;
import static org.springframework.core.annotation.AnnotatedElementUtils.hasAnnotation;
/**
* Collection of utilities related to autowiring of individual method parameters.
*
* @author Sam Brannen
* @since 5.0
* @see MethodParameterFactory
* @see #isAutowirable(Parameter)
* @see #resolveDependency(Parameter, Class, ApplicationContext)
*/
public abstract class ParameterAutowireUtils {
private ParameterAutowireUtils() {
/* no-op */
}
/**
* Determine if the supplied {@link Parameter} can potentially be
* autowired from an {@link ApplicationContext}.
* <p>Returns {@code true} if the supplied parameter is of type
* {@link ApplicationContext} (or a sub-type thereof) or is annotated or
* meta-annotated with {@link Autowired @Autowired},
* {@link Qualifier @Qualifier}, or {@link Value @Value}.
* @see #resolveDependency(Parameter, Class, ApplicationContext)
*/
public static boolean isAutowirable(Parameter parameter) {
return ApplicationContext.class.isAssignableFrom(parameter.getType())
|| hasAnnotation(parameter, Autowired.class)
|| hasAnnotation(parameter, Qualifier.class)
|| hasAnnotation(parameter, Value.class);
}
/**
* Resolve the dependency for the supplied {@link Parameter} from the
* supplied {@link ApplicationContext}.
* <p>Provides comprehensive autowiring support for individual method parameters
* on par with Spring's dependency injection facilities for autowired fields and
* methods, including support for {@link Autowired @Autowired},
* {@link Qualifier @Qualifier}, and {@link Value @Value} with support for property
* placeholders and SpEL expressions in {@code @Value} declarations.
* <p>The dependency is required unless the parameter is annotated with
* {@link Autowired @Autowired} with the {@link Autowired#required required}
* flag set to {@code false}.
* <p>If an explicit <em>qualifier</em> is not declared, the name of the parameter
* will be used as the qualifier for resolving ambiguities.
* @param parameter the parameter whose dependency should be resolved
* @param containingClass the concrete class that contains the parameter; this may
* differ from the class that declares the parameter in that it may be a subclass
* thereof, potentially substituting type variables
* @param applicationContext the application context from which to resolve the
* dependency
* @return the resolved object, or {@code null} if none found
* @throws BeansException if dependency resolution failed
* @see #isAutowirable(Parameter)
* @see Autowired#required
* @see MethodParameterFactory#createSynthesizingMethodParameter(Parameter)
* @see AutowireCapableBeanFactory#resolveDependency(DependencyDescriptor, String)
*/
public static Object resolveDependency(Parameter parameter, Class<?> containingClass,
ApplicationContext applicationContext) {
boolean required = findMergedAnnotation(parameter, Autowired.class).map(Autowired::required).orElse(true);
MethodParameter methodParameter = (parameter.getDeclaringExecutable() instanceof Method
? MethodParameterFactory.createSynthesizingMethodParameter(parameter)
: MethodParameterFactory.createMethodParameter(parameter));
DependencyDescriptor descriptor = new DependencyDescriptor(methodParameter, required);
descriptor.setContainingClass(containingClass);
return applicationContext.getAutowireCapableBeanFactory().resolveDependency(descriptor, null);
}
private static <A extends Annotation> Optional<A> findMergedAnnotation(AnnotatedElement element,
Class<A> annotationType) {
return Optional.ofNullable(AnnotatedElementUtils.findMergedAnnotation(element, annotationType));
}
}

View File

@@ -0,0 +1,5 @@
/**
* Internal support classes for integrating the <em>Spring TestContext Framework</em>
* with the JUnit Jupiter extension model in JUnit 5.
*/
package org.springframework.test.context.junit.jupiter.support;

View File

@@ -0,0 +1,107 @@
/*
* Copyright 2002-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.test.context.junit.jupiter.web;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.AliasFor;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.context.web.WebAppConfiguration;
/**
* {@code @SpringJUnitWebConfig} is a <em>composed annotation</em> that combines
* {@link ExtendWith @ExtendWith(SpringExtension.class)} from JUnit Jupiter with
* {@link ContextConfiguration @ContextConfiguration} and
* {@link WebAppConfiguration @WebAppConfiguration} from the <em>Spring TestContext
* Framework</em>.
*
* @author Sam Brannen
* @since 5.0
* @see ExtendWith
* @see SpringExtension
* @see ContextConfiguration
* @see WebAppConfiguration
* @see org.springframework.test.context.junit.jupiter.SpringJUnitConfig
*/
@ExtendWith(SpringExtension.class)
@ContextConfiguration
@WebAppConfiguration
@Documented
@Inherited
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface SpringJUnitWebConfig {
/**
* Alias for {@link ContextConfiguration#classes}.
*/
@AliasFor(annotation = ContextConfiguration.class, attribute = "classes")
Class<?>[] value() default {};
/**
* Alias for {@link ContextConfiguration#classes}.
*/
@AliasFor(annotation = ContextConfiguration.class)
Class<?>[] classes() default {};
/**
* Alias for {@link ContextConfiguration#locations}.
*/
@AliasFor(annotation = ContextConfiguration.class)
String[] locations() default {};
/**
* Alias for {@link ContextConfiguration#initializers}.
*/
@AliasFor(annotation = ContextConfiguration.class)
Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[] initializers() default {};
/**
* Alias for {@link ContextConfiguration#inheritLocations}.
*/
@AliasFor(annotation = ContextConfiguration.class)
boolean inheritLocations() default true;
/**
* Alias for {@link ContextConfiguration#inheritInitializers}.
*/
@AliasFor(annotation = ContextConfiguration.class)
boolean inheritInitializers() default true;
/**
* Alias for {@link ContextConfiguration#name}.
*/
@AliasFor(annotation = ContextConfiguration.class)
String name() default "";
/**
* Alias for {@link WebAppConfiguration#value}.
*/
@AliasFor(annotation = WebAppConfiguration.class, attribute = "value")
String resourcePath() default "src/main/webapp";
}

View File

@@ -0,0 +1,5 @@
/**
* Web support for integrating the <em>Spring TestContext Framework</em>
* with the JUnit Jupiter extension model in JUnit 5.
*/
package org.springframework.test.context.junit.jupiter.web;