diff options
Diffstat (limited to 'spring-test/src/main/java/org/springframework')
95 files changed, 2782 insertions, 1006 deletions
diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java index 01840d66..d1a8c37c 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java @@ -390,8 +390,8 @@ public class MockHttpServletRequest implements HttpServletRequest { if (contentType != null) { try { MediaType mediaType = MediaType.parseMediaType(contentType); - if (mediaType.getCharSet() != null) { - this.characterEncoding = mediaType.getCharSet().name(); + if (mediaType.getCharset() != null) { + this.characterEncoding = mediaType.getCharset().name(); } } catch (Exception ex) { diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java index 5fc724b8..1d5b838d 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java @@ -236,8 +236,8 @@ public class MockHttpServletResponse implements HttpServletResponse { if (contentType != null) { try { MediaType mediaType = MediaType.parseMediaType(contentType); - if (mediaType.getCharSet() != null) { - this.characterEncoding = mediaType.getCharSet().name(); + if (mediaType.getCharset() != null) { + this.characterEncoding = mediaType.getCharset().name(); this.charset = true; } } diff --git a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java index 8be8a9a1..7bca348a 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -25,7 +25,6 @@ import java.util.Collections; import java.util.Enumeration; import java.util.EventListener; import java.util.HashMap; -import java.util.HashSet; import java.util.LinkedHashMap; import java.util.LinkedHashSet; import java.util.Map; @@ -144,7 +143,7 @@ public class MockServletContext implements ServletContext { private String servletContextName = "MockServletContext"; - private final Set<String> declaredRoles = new HashSet<String>(); + private final Set<String> declaredRoles = new LinkedHashSet<String>(); private Set<SessionTrackingMode> sessionTrackingModes; @@ -370,7 +369,6 @@ public class MockServletContext implements ServletContext { /** * Register a {@link RequestDispatcher} (typically a {@link MockRequestDispatcher}) * that acts as a wrapper for the named Servlet. - * * @param name the name of the wrapped Servlet * @param requestDispatcher the dispatcher that wraps the named Servlet * @see #getNamedDispatcher @@ -384,7 +382,6 @@ public class MockServletContext implements ServletContext { /** * Unregister the {@link RequestDispatcher} with the given name. - * * @param name the name of the dispatcher to unregister * @see #getNamedDispatcher * @see #registerNamedDispatcher @@ -429,13 +426,13 @@ public class MockServletContext implements ServletContext { @Override @Deprecated public Enumeration<Servlet> getServlets() { - return Collections.enumeration(new HashSet<Servlet>()); + return Collections.enumeration(Collections.<Servlet>emptySet()); } @Override @Deprecated public Enumeration<String> getServletNames() { - return Collections.enumeration(new HashSet<String>()); + return Collections.enumeration(Collections.<String>emptySet()); } @Override diff --git a/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java index 66a663e0..716f71b0 100644 --- a/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java +++ b/spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -21,7 +21,6 @@ import java.net.MalformedURLException; import java.net.URL; import java.util.Collections; import java.util.Enumeration; -import java.util.HashSet; import java.util.Set; import javax.portlet.PortletContext; import javax.portlet.PortletRequestDispatcher; @@ -156,7 +155,7 @@ public class ServletWrappingPortletContext implements PortletContext { @Override public Enumeration<String> getContainerRuntimeOptions() { - return Collections.enumeration(new HashSet<String>()); + return Collections.enumeration(Collections.<String>emptySet()); } } diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java b/spring-test/src/main/java/org/springframework/test/annotation/Commit.java index e23c254f..98b3d96b 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Commit.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Commit.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.test.annotation; 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; @@ -51,6 +52,7 @@ import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited @Rollback(false) public @interface Commit { } diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java index cd285587..e5f61e45 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -24,7 +24,7 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * <p>{@code ProfileValueSourceConfiguration} is a class-level annotation which + * {@code ProfileValueSourceConfiguration} is a class-level annotation which * is used to specify what type of {@link ProfileValueSource} to use when * retrieving <em>profile values</em> configured via the {@link IfProfileValue * @IfProfileValue} annotation. @@ -38,17 +38,15 @@ import java.lang.annotation.Target; * @see IfProfileValue * @see ProfileValueUtils */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface ProfileValueSourceConfiguration { /** - * <p> * The type of {@link ProfileValueSource} to use when retrieving * <em>profile values</em>. - * </p> * * @see SystemProfileValueSource */ diff --git a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java index 27ce7f64..17c60cad 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -21,13 +21,12 @@ import java.lang.reflect.Method; 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.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; - /** * General utility methods for working with <em>profile values</em>. * @@ -49,12 +48,10 @@ public abstract class ProfileValueUtils { * {@link ProfileValueSourceConfiguration * @ProfileValueSourceConfiguration} annotation and instantiates a new * instance of that type. - * <p> - * If {@link ProfileValueSourceConfiguration + * <p>If {@link ProfileValueSourceConfiguration * @ProfileValueSourceConfiguration} is not present on the specified * class or if a custom {@link ProfileValueSource} is not declared, the * default {@link SystemProfileValueSource} will be returned instead. - * * @param testClass The test class for which the ProfileValueSource should * be retrieved * @return the configured (or default) ProfileValueSource for the specified @@ -66,10 +63,10 @@ public abstract class ProfileValueUtils { Assert.notNull(testClass, "testClass must not be null"); Class<ProfileValueSourceConfiguration> annotationType = ProfileValueSourceConfiguration.class; - ProfileValueSourceConfiguration config = findAnnotation(testClass, annotationType); + ProfileValueSourceConfiguration config = AnnotatedElementUtils.findMergedAnnotation(testClass, annotationType); if (logger.isDebugEnabled()) { - logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class [" - + testClass.getName() + "]"); + logger.debug("Retrieved @ProfileValueSourceConfiguration [" + config + "] for test class [" + + testClass.getName() + "]"); } Class<? extends ProfileValueSource> profileValueSourceType; @@ -80,8 +77,8 @@ public abstract class ProfileValueUtils { profileValueSourceType = (Class<? extends ProfileValueSource>) AnnotationUtils.getDefaultValue(annotationType); } if (logger.isDebugEnabled()) { - logger.debug("Retrieved ProfileValueSource type [" + profileValueSourceType + "] for class [" - + testClass.getName() + "]"); + logger.debug("Retrieved ProfileValueSource type [" + profileValueSourceType + "] for class [" + + testClass.getName() + "]"); } ProfileValueSource profileValueSource; @@ -92,10 +89,10 @@ public abstract class ProfileValueUtils { try { profileValueSource = profileValueSourceType.newInstance(); } - catch (Exception e) { + catch (Exception ex) { if (logger.isWarnEnabled()) { - logger.warn("Could not instantiate a ProfileValueSource of type [" + profileValueSourceType - + "] for class [" + testClass.getName() + "]: using default.", e); + logger.warn("Could not instantiate a ProfileValueSource of type [" + profileValueSourceType + + "] for class [" + testClass.getName() + "]: using default.", ex); } profileValueSource = SystemProfileValueSource.getInstance(); } @@ -108,16 +105,14 @@ public abstract class ProfileValueUtils { * Determine if the supplied {@code testClass} is <em>enabled</em> in * the current environment, as specified by the {@link IfProfileValue * @IfProfileValue} annotation at the class level. - * <p> - * Defaults to {@code true} if no {@link IfProfileValue + * <p>Defaults to {@code true} if no {@link IfProfileValue * @IfProfileValue} annotation is declared. - * * @param testClass the test class * @return {@code true} if the test is <em>enabled</em> in the current * environment */ public static boolean isTestEnabledInThisEnvironment(Class<?> testClass) { - IfProfileValue ifProfileValue = findAnnotation(testClass, IfProfileValue.class); + IfProfileValue ifProfileValue = AnnotatedElementUtils.findMergedAnnotation(testClass, IfProfileValue.class); return isTestEnabledInThisEnvironment(retrieveProfileValueSource(testClass), ifProfileValue); } @@ -127,10 +122,8 @@ public abstract class ProfileValueUtils { * @IfProfileValue} annotation, which may be declared on the test * method itself or at the class level. Class-level usage overrides * method-level usage. - * <p> - * Defaults to {@code true} if no {@link IfProfileValue + * <p>Defaults to {@code true} if no {@link IfProfileValue * @IfProfileValue} annotation is declared. - * * @param testMethod the test method * @param testClass the test class * @return {@code true} if the test is <em>enabled</em> in the current @@ -146,10 +139,8 @@ public abstract class ProfileValueUtils { * @IfProfileValue} annotation, which may be declared on the test * method itself or at the class level. Class-level usage overrides * method-level usage. - * <p> - * Defaults to {@code true} if no {@link IfProfileValue + * <p>Defaults to {@code true} if no {@link IfProfileValue * @IfProfileValue} annotation is declared. - * * @param profileValueSource the ProfileValueSource to use to determine if * the test is enabled * @param testMethod the test method @@ -160,11 +151,11 @@ public abstract class ProfileValueUtils { public static boolean isTestEnabledInThisEnvironment(ProfileValueSource profileValueSource, Method testMethod, Class<?> testClass) { - IfProfileValue ifProfileValue = findAnnotation(testClass, IfProfileValue.class); + IfProfileValue ifProfileValue = AnnotatedElementUtils.findMergedAnnotation(testClass, IfProfileValue.class); boolean classLevelEnabled = isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue); if (classLevelEnabled) { - ifProfileValue = findAnnotation(testMethod, IfProfileValue.class); + ifProfileValue = AnnotatedElementUtils.findMergedAnnotation(testMethod, IfProfileValue.class); return isTestEnabledInThisEnvironment(profileValueSource, ifProfileValue); } @@ -175,7 +166,6 @@ public abstract class ProfileValueUtils { * Determine if the {@code value} (or one of the {@code values}) * in the supplied {@link IfProfileValue @IfProfileValue} annotation is * <em>enabled</em> in the current environment. - * * @param profileValueSource the ProfileValueSource to use to determine if * the test is enabled * @param ifProfileValue the annotation to introspect; may be @@ -195,8 +185,8 @@ public abstract class ProfileValueUtils { String[] annotatedValues = ifProfileValue.values(); if (StringUtils.hasLength(ifProfileValue.value())) { if (annotatedValues.length > 0) { - throw new IllegalArgumentException("Setting both the 'value' and 'values' attributes " - + "of @IfProfileValue is not allowed: choose one or the other."); + throw new IllegalArgumentException("Setting both the 'value' and 'values' attributes " + + "of @IfProfileValue is not allowed: choose one or the other."); } annotatedValues = new String[] { ifProfileValue.value() }; } diff --git a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java index 37bf2102..c1f06e64 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/Rollback.java @@ -18,6 +18,7 @@ package org.springframework.test.annotation; 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; @@ -56,6 +57,7 @@ import java.lang.annotation.Target; @Target({ElementType.TYPE, ElementType.METHOD}) @Retention(RetentionPolicy.RUNTIME) @Documented +@Inherited public @interface Rollback { /** diff --git a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java index 5f4eb1c2..a2b33faa 100644 --- a/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -19,7 +19,6 @@ package org.springframework.test.annotation; import java.lang.reflect.Method; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationUtils; /** * Collection of utility methods for working with Spring's core testing annotations. @@ -52,7 +51,7 @@ public class TestAnnotationUtils { * not annotated with {@code @Repeat} */ public static int getRepeatCount(Method method) { - Repeat repeat = AnnotationUtils.findAnnotation(method, Repeat.class); + Repeat repeat = AnnotatedElementUtils.findMergedAnnotation(method, Repeat.class); if (repeat == null) { return 1; } diff --git a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java index fc259102..54b08892 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java +++ b/spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java @@ -43,10 +43,10 @@ import org.springframework.core.annotation.AliasFor; * @see org.springframework.context.ApplicationContext * @see org.springframework.context.annotation.Profile */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface ActiveProfiles { /** 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 c16d7def..22358071 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,22 +17,22 @@ package org.springframework.test.context; import java.lang.reflect.Constructor; -import java.util.List; +import java.util.Set; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanUtils; import org.springframework.core.annotation.AnnotatedElementUtils; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotationAttributes; import org.springframework.util.ClassUtils; -import org.springframework.util.MultiValueMap; /** * {@code BootstrapUtils} is a collection of utility methods to assist with * bootstrapping the <em>Spring TestContext Framework</em>. * * @author Sam Brannen + * @author Phillip Webb * @since 4.1 * @see BootstrapWith * @see BootstrapContext @@ -49,6 +49,12 @@ abstract class BootstrapUtils { private static final String DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = "org.springframework.test.context.support.DefaultTestContextBootstrapper"; + private static final String DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME = + "org.springframework.test.context.web.WebTestContextBootstrapper"; + + private static final String WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME = + "org.springframework.test.context.web.WebAppConfiguration"; + private static final Log logger = LogFactory.getLog(BootstrapUtils.class); @@ -103,51 +109,64 @@ abstract class BootstrapUtils { * <p>If the {@link BootstrapWith @BootstrapWith} annotation is present on * the test class, either directly or as a meta-annotation, then its * {@link BootstrapWith#value value} will be used as the bootstrapper type. - * Otherwise, the {@link org.springframework.test.context.support.DefaultTestContextBootstrapper - * DefaultTestContextBootstrapper} will be used. + * Otherwise, either the + * {@link org.springframework.test.context.support.DefaultTestContextBootstrapper + * DefaultTestContextBootstrapper} or the + * {@link org.springframework.test.context.web.WebTestContextBootstrapper + * WebTestContextBootstrapper} will be used, depending on the presence of + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}. * @param bootstrapContext the bootstrap context to use * @return a fully configured {@code TestContextBootstrapper} */ - @SuppressWarnings("unchecked") static TestContextBootstrapper resolveTestContextBootstrapper(BootstrapContext bootstrapContext) { Class<?> testClass = bootstrapContext.getTestClass(); - Class<? extends TestContextBootstrapper> clazz = null; + Class<?> clazz = null; try { - MultiValueMap<String, Object> attributesMultiMap = AnnotatedElementUtils.getAllAnnotationAttributes( - testClass, BootstrapWith.class.getName()); - List<Object> values = (attributesMultiMap == null ? null : attributesMultiMap.get(AnnotationUtils.VALUE)); - - if (values != null) { - if (values.size() != 1) { - throw new IllegalStateException(String.format("Configuration error: found multiple declarations of " + - "@BootstrapWith on test class [%s] with values %s", testClass.getName(), values)); - } - clazz = (Class<? extends TestContextBootstrapper>) values.get(0); - } - else { - clazz = (Class<? extends TestContextBootstrapper>) ClassUtils.forName( - DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, BootstrapUtils.class.getClassLoader()); + clazz = resolveExplicitTestContextBootstrapper(testClass); + if (clazz == null) { + clazz = resolveDefaultTestContextBootstrapper(testClass); } - if (logger.isDebugEnabled()) { logger.debug(String.format("Instantiating TestContextBootstrapper for test class [%s] from class [%s]", testClass.getName(), clazz.getName())); } - TestContextBootstrapper testContextBootstrapper = BeanUtils.instantiateClass(clazz, TestContextBootstrapper.class); testContextBootstrapper.setBootstrapContext(bootstrapContext); return testContextBootstrapper; } + catch (IllegalStateException ex) { + throw ex; + } catch (Throwable ex) { - if (ex instanceof IllegalStateException) { - throw (IllegalStateException) ex; - } throw new IllegalStateException("Could not load TestContextBootstrapper [" + clazz + "]. Specify @BootstrapWith's 'value' attribute or make the default bootstrapper class available.", ex); } } + private static Class<?> resolveExplicitTestContextBootstrapper(Class<?> testClass) { + Set<BootstrapWith> annotations = AnnotatedElementUtils.findAllMergedAnnotations(testClass, BootstrapWith.class); + if (annotations.size() < 1) { + return null; + } + if (annotations.size() > 1) { + throw new IllegalStateException(String.format( + "Configuration error: found multiple declarations of @BootstrapWith for test class [%s]: %s", + testClass.getName(), annotations)); + } + return annotations.iterator().next().value(); + } + + private static Class<?> resolveDefaultTestContextBootstrapper(Class<?> testClass) throws Exception { + ClassLoader classLoader = BootstrapUtils.class.getClassLoader(); + AnnotationAttributes attributes = AnnotatedElementUtils.findMergedAnnotationAttributes(testClass, + WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false); + if (attributes != null) { + return ClassUtils.forName(DEFAULT_WEB_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader); + } + return ClassUtils.forName(DEFAULT_TEST_CONTEXT_BOOTSTRAPPER_CLASS_NAME, classLoader); + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java index 302ce42e..7cf742f6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java +++ b/spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -35,10 +35,10 @@ import java.lang.annotation.Target; * @see BootstrapContext * @see TestContextBootstrapper */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface BootstrapWith { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java index 9087bdbe..669adbd6 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -83,10 +83,10 @@ import org.springframework.core.annotation.AliasFor; * @see MergedContextConfiguration * @see org.springframework.context.ApplicationContext */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface ContextConfiguration { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java index 0e0feb47..f32731df 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -34,6 +34,7 @@ import org.springframework.util.StringUtils; * attributes declared via {@link ContextConfiguration @ContextConfiguration}. * * @author Sam Brannen + * @author Phillip Webb * @since 3.1 * @see ContextConfiguration * @see SmartContextLoader#processContextConfiguration(ContextConfigurationAttributes) @@ -41,6 +42,11 @@ import org.springframework.util.StringUtils; */ public class ContextConfigurationAttributes { + private static final String[] EMPTY_LOCATIONS = new String[0]; + + private static final Class<?>[] EMPTY_CLASSES = new Class<?>[0]; + + private static final Log logger = LogFactory.getLog(ContextConfigurationAttributes.class); private final Class<?> declaringClass; @@ -61,6 +67,17 @@ public class ContextConfigurationAttributes { /** + * Construct a new {@link ContextConfigurationAttributes} instance with default values. + * @param declaringClass the test class that declared {@code @ContextConfiguration}, + * either explicitly or implicitly + * @since 4.3 + */ + @SuppressWarnings("unchecked") + public ContextConfigurationAttributes(Class<?> declaringClass) { + this(declaringClass, EMPTY_LOCATIONS, EMPTY_CLASSES, false, (Class[]) EMPTY_CLASSES, true, ContextLoader.class); + } + + /** * Construct a new {@link ContextConfigurationAttributes} instance for the * supplied {@link ContextConfiguration @ContextConfiguration} annotation and * the {@linkplain Class test class} that declared it. @@ -84,8 +101,8 @@ public class ContextConfigurationAttributes { @SuppressWarnings("unchecked") public ContextConfigurationAttributes(Class<?> declaringClass, AnnotationAttributes annAttrs) { this(declaringClass, annAttrs.getStringArray("locations"), annAttrs.getClassArray("classes"), annAttrs.getBoolean("inheritLocations"), - (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"), - annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), (Class<? extends ContextLoader>) annAttrs.getClass("loader")); + (Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>[]) annAttrs.getClassArray("initializers"), + annAttrs.getBoolean("inheritInitializers"), annAttrs.getString("name"), (Class<? extends ContextLoader>) annAttrs.getClass("loader")); } /** @@ -139,8 +156,8 @@ public class ContextConfigurationAttributes { if (!ObjectUtils.isEmpty(locations) && !ObjectUtils.isEmpty(classes) && logger.isDebugEnabled()) { logger.debug(String.format( "Test class [%s] has been configured with @ContextConfiguration's 'locations' (or 'value') %s " + - "and 'classes' %s attributes. Most SmartContextLoader implementations support " + - "only one declaration of resources per @ContextConfiguration annotation.", + "and 'classes' %s attributes. Most SmartContextLoader implementations support " + + "only one declaration of resources per @ContextConfiguration annotation.", declaringClass.getName(), ObjectUtils.nullSafeToString(locations), ObjectUtils.nullSafeToString(classes))); } @@ -158,7 +175,8 @@ public class ContextConfigurationAttributes { /** * Get the {@linkplain Class class} that declared the - * {@link ContextConfiguration @ContextConfiguration} annotation. + * {@link ContextConfiguration @ContextConfiguration} annotation, either explicitly + * or implicitly. * @return the declaring class (never {@code null}) */ public Class<?> getDeclaringClass() { diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java new file mode 100644 index 00000000..d9462faa --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java @@ -0,0 +1,49 @@ +/* + * 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; + +import org.springframework.context.ConfigurableApplicationContext; + +/** + * Strategy interface for customizing {@link ConfigurableApplicationContext + * application contexts} that are created and managed by the <em>Spring + * TestContext Framework</em>. + * + * <p>Customizers are created by {@link ContextCustomizerFactory} implementations. + * + * <p>Implementations must implement correct {@code equals} and {@code hashCode} + * methods since customizers form part of the {@link MergedContextConfiguration} + * which is used as a cache key. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 4.3 + * @see ContextCustomizerFactory + * @see org.springframework.test.context.support.AbstractContextLoader#customizeContext + */ +public interface ContextCustomizer { + + /** + * Customize the supplied {@code ConfigurableApplicationContext} <em>after</em> + * bean definitions have been loaded into the context but <em>before</em> the + * context has been refreshed. + * @param context the context to customize + * @param mergedConfig the merged context configuration + */ + void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java new file mode 100644 index 00000000..9f07700a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java @@ -0,0 +1,52 @@ +/* + * 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; + +import java.util.List; + +/** + * Factory for creating {@link ContextCustomizer ContextCustomizers}. + * + * <p>Factories are invoked after {@link ContextLoader ContextLoaders} have + * processed context configuration attributes but before the + * {@link MergedContextConfiguration} is created. + * + * <p>By default, the Spring TestContext Framework will use the + * {@link org.springframework.core.io.support.SpringFactoriesLoader SpringFactoriesLoader} + * mechanism for loading factories configured in all {@code META-INF/spring.factories} + * files on the classpath. + * + * @author Phillip Webb + * @author Sam Brannen + * @since 4.3 + */ +public interface ContextCustomizerFactory { + + /** + * Create a {@link ContextCustomizer} that should be used to customize a + * {@link org.springframework.context.ConfigurableApplicationContext ConfigurableApplicationContext} + * before it is refreshed. + * @param testClass the test class + * @param configAttributes the list of context configuration attributes for + * the test class, ordered <em>bottom-up</em> (i.e., as if we were traversing + * up the class hierarchy); never {@code null} or empty + * @return a {@link ContextCustomizer} or {@code null} if no customizer should + * be used + */ + ContextCustomizer createContextCustomizer(Class<?> testClass, List<ContextConfigurationAttributes> configAttributes); + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java index 61b2fa8c..3a3d2859 100644 --- a/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java +++ b/spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -139,10 +139,10 @@ import java.lang.annotation.Target; * @see ContextConfiguration * @see org.springframework.context.ApplicationContext */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface ContextHierarchy { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java index 5090d7c5..45f31a18 100644 --- a/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java @@ -55,6 +55,7 @@ import org.springframework.util.StringUtils; * that was loaded using properties of this {@code MergedContextConfiguration}. * * @author Sam Brannen + * @author Phillip Webb * @since 3.1 * @see ContextConfiguration * @see ContextHierarchy @@ -74,6 +75,8 @@ public class MergedContextConfiguration implements Serializable { private static final Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> EMPTY_INITIALIZER_CLASSES = Collections.<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> emptySet(); + private static final Set<ContextCustomizer> EMPTY_CONTEXT_CUSTOMIZERS = Collections.<ContextCustomizer> emptySet(); + private final Class<?> testClass; @@ -89,6 +92,8 @@ public class MergedContextConfiguration implements Serializable { private final String[] propertySourceProperties; + private final Set<ContextCustomizer> contextCustomizers; + private final ContextLoader contextLoader; private final CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate; @@ -111,6 +116,11 @@ public class MergedContextConfiguration implements Serializable { Collections.unmodifiableSet(contextInitializerClasses) : EMPTY_INITIALIZER_CLASSES); } + private static Set<ContextCustomizer> processContextCustomizers(Set<ContextCustomizer> contextCustomizers) { + return (contextCustomizers != null ? + Collections.unmodifiableSet(contextCustomizers) : EMPTY_CONTEXT_CUSTOMIZERS); + } + private static String[] processActiveProfiles(String[] activeProfiles) { if (activeProfiles == null) { return EMPTY_STRING_ARRAY; @@ -201,8 +211,8 @@ public class MergedContextConfiguration implements Serializable { public MergedContextConfiguration(MergedContextConfiguration mergedConfig) { this(mergedConfig.testClass, mergedConfig.locations, mergedConfig.classes, mergedConfig.contextInitializerClasses, mergedConfig.activeProfiles, mergedConfig.propertySourceLocations, - mergedConfig.propertySourceProperties, mergedConfig.contextLoader, - mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent); + mergedConfig.propertySourceProperties, mergedConfig.contextCustomizers, + mergedConfig.contextLoader, mergedConfig.cacheAwareContextLoaderDelegate, mergedConfig.parent); } /** @@ -233,6 +243,41 @@ public class MergedContextConfiguration implements Serializable { String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, + propertySourceLocations, propertySourceProperties, + EMPTY_CONTEXT_CUSTOMIZERS, contextLoader, + cacheAwareContextLoaderDelegate, parent); + } + + /** + * Create a new {@code MergedContextConfiguration} instance for the + * supplied parameters. + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations}, + * or {@code propertySourceProperties} an empty array will be stored instead. + * If a {@code null} value is supplied for {@code contextInitializerClasses} + * or {@code contextCustomizers}, an empty set will be stored instead. + * Furthermore, active profiles will be sorted, and duplicate profiles + * will be removed. + * @param testClass the test class for which the configuration was merged + * @param locations the merged context resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param propertySourceLocations the merged {@code PropertySource} locations + * @param propertySourceProperties the merged {@code PropertySource} properties + * @param contextCustomizers the context customizers + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent context + * @param parent the parent configuration or {@code null} if there is no parent + * @since 4.3 + */ + public MergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties, + Set<ContextCustomizer> contextCustomizers, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { this.testClass = testClass; this.locations = processStrings(locations); @@ -241,6 +286,7 @@ public class MergedContextConfiguration implements Serializable { this.activeProfiles = processActiveProfiles(activeProfiles); this.propertySourceLocations = processStrings(propertySourceLocations); this.propertySourceProperties = processStrings(propertySourceProperties); + this.contextCustomizers = processContextCustomizers(contextCustomizers); this.contextLoader = contextLoader; this.cacheAwareContextLoaderDelegate = cacheAwareContextLoaderDelegate; this.parent = parent; @@ -349,6 +395,14 @@ public class MergedContextConfiguration implements Serializable { } /** + * Get the merged {@link ContextCustomizer ContextCustomizers} that will be applied + * when the application context is loaded. + */ + public Set<ContextCustomizer> getContextCustomizers() { + return this.contextCustomizers; + } + + /** * Get the resolved {@link ContextLoader} for the {@linkplain #getTestClass() test class}. */ public ContextLoader getContextLoader() { @@ -424,6 +478,9 @@ public class MergedContextConfiguration implements Serializable { if (!Arrays.equals(this.propertySourceProperties, otherConfig.propertySourceProperties)) { return false; } + if (!this.contextCustomizers.equals(otherConfig.contextCustomizers)) { + return false; + } if (this.parent == null) { if (otherConfig.parent != null) { @@ -454,6 +511,7 @@ public class MergedContextConfiguration implements Serializable { result = 31 * result + Arrays.hashCode(this.activeProfiles); result = 31 * result + Arrays.hashCode(this.propertySourceLocations); result = 31 * result + Arrays.hashCode(this.propertySourceProperties); + result = 31 * result + this.contextCustomizers.hashCode(); result = 31 * result + (this.parent != null ? this.parent.hashCode() : 0); result = 31 * result + nullSafeToString(this.contextLoader).hashCode(); return result; @@ -466,6 +524,7 @@ public class MergedContextConfiguration implements Serializable { * {@linkplain #getActiveProfiles() active profiles}, * {@linkplain #getPropertySourceLocations() property source locations}, * {@linkplain #getPropertySourceProperties() property source properties}, + * {@linkplain #getContextCustomizers() context customizers}, * the name of the {@link #getContextLoader() ContextLoader}, and the * {@linkplain #getParent() parent configuration}. */ @@ -479,6 +538,7 @@ public class MergedContextConfiguration implements Serializable { .append("activeProfiles", ObjectUtils.nullSafeToString(this.activeProfiles)) .append("propertySourceLocations", ObjectUtils.nullSafeToString(this.propertySourceLocations)) .append("propertySourceProperties", ObjectUtils.nullSafeToString(this.propertySourceProperties)) + .append("contextCustomizers", this.contextCustomizers) .append("contextLoader", nullSafeToString(this.contextLoader)) .append("parent", this.parent) .toString(); diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java index 2ec2b7d4..905ed70b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -31,13 +31,14 @@ import java.util.List; * * <p>A custom bootstrapping strategy can be configured for a test class (or * test class hierarchy) via {@link BootstrapWith @BootstrapWith}, either - * directly or as a meta-annotation. See - * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration} - * for an example. + * directly or as a meta-annotation. * - * <p>If a bootstrapper is not explicitly configured via {@code @BootstrapWith}, the - * {@link org.springframework.test.context.support.DefaultTestContextBootstrapper DefaultTestContextBootstrapper} - * will be used. + * <p>If a bootstrapper is not explicitly configured via {@code @BootstrapWith}, + * either the {@link org.springframework.test.context.support.DefaultTestContextBootstrapper + * DefaultTestContextBootstrapper} or the + * {@link org.springframework.test.context.web.WebTestContextBootstrapper + * WebTestContextBootstrapper} will be used, depending on the presence of + * {@link org.springframework.test.context.web.WebAppConfiguration @WebAppConfiguration}. * * <h3>Implementation Notes</h3> * diff --git a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java index 3fd77678..28979647 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestContextManager.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -25,6 +25,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; /** * {@code TestContextManager} is the main entry point into the <em>Spring @@ -37,18 +38,18 @@ import org.springframework.util.Assert; * * <ul> * <li>{@link #beforeTestClass() before test class execution}: prior to any - * <em>before class methods</em> of a particular testing framework (e.g., JUnit - * 4's {@link org.junit.BeforeClass @BeforeClass})</li> + * <em>before class callbacks</em> of a particular testing framework (e.g., + * JUnit 4's {@link org.junit.BeforeClass @BeforeClass})</li> * <li>{@link #prepareTestInstance(Object) test instance preparation}: * immediately following instantiation of the test instance</li> * <li>{@link #beforeTestMethod(Object, Method) before test method execution}: - * prior to any <em>before methods</em> of a particular testing framework (e.g., - * JUnit 4's {@link org.junit.Before @Before})</li> + * prior to any <em>before method callbacks</em> of a particular testing framework + * (e.g., JUnit 4's {@link org.junit.Before @Before})</li> * <li>{@link #afterTestMethod(Object, Method, Throwable) after test method - * execution}: after any <em>after methods</em> of a particular testing + * execution}: after any <em>after method callbacks</em> of a particular testing * framework (e.g., JUnit 4's {@link org.junit.After @After})</li> * <li>{@link #afterTestClass() after test class execution}: after any - * <em>after class methods</em> of a particular testing framework (e.g., JUnit + * <em>after class callbacks</em> of a particular testing framework (e.g., JUnit * 4's {@link org.junit.AfterClass @AfterClass})</li> * </ul> * @@ -78,7 +79,6 @@ import org.springframework.util.Assert; * @see TestExecutionListeners * @see ContextConfiguration * @see ContextHierarchy - * @see org.springframework.test.context.transaction.TransactionConfiguration */ public class TestContextManager { @@ -124,7 +124,7 @@ public class TestContextManager { /** * Get the {@link TestContext} managed by this {@code TestContextManager}. */ - protected final TestContext getTestContext() { + public final TestContext getTestContext() { return this.testContext; } @@ -194,10 +194,12 @@ public class TestContextManager { try { testExecutionListener.beforeTestClass(getTestContext()); } - catch (Exception ex) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'before class' callback for test class [" + testClass + "]", ex); - throw ex; + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'before class' callback for test class [" + testClass + "]", ex); + } + ReflectionUtils.rethrowException(ex); } } } @@ -227,10 +229,12 @@ public class TestContextManager { try { testExecutionListener.prepareTestInstance(getTestContext()); } - catch (Exception ex) { - logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to prepare test instance [" + testInstance + "]", ex); - throw ex; + catch (Throwable ex) { + if (logger.isErrorEnabled()) { + logger.error("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to prepare test instance [" + testInstance + "]", ex); + } + ReflectionUtils.rethrowException(ex); } } } @@ -264,11 +268,13 @@ public class TestContextManager { try { testExecutionListener.beforeTestMethod(getTestContext()); } - catch (Exception ex) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'before' execution of test method [" + testMethod + "] for test instance [" + - testInstance + "]", ex); - throw ex; + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'before' execution of test method [" + testMethod + "] for test instance [" + + testInstance + "]", ex); + } + ReflectionUtils.rethrowException(ex); } } } @@ -305,24 +311,26 @@ public class TestContextManager { } getTestContext().updateState(testInstance, testMethod, exception); - Exception afterTestMethodException = null; + Throwable afterTestMethodException = null; // Traverse the TestExecutionListeners in reverse order to ensure proper // "wrapper"-style execution of listeners. for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { try { testExecutionListener.afterTestMethod(getTestContext()); } - catch (Exception ex) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'after' execution for test: method [" + testMethod + "], instance [" + - testInstance + "], exception [" + exception + "]", ex); + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'after' execution for test: method [" + testMethod + "], instance [" + + testInstance + "], exception [" + exception + "]", ex); + } if (afterTestMethodException == null) { afterTestMethodException = ex; } } } if (afterTestMethodException != null) { - throw afterTestMethodException; + ReflectionUtils.rethrowException(afterTestMethodException); } } @@ -347,23 +355,25 @@ public class TestContextManager { } getTestContext().updateState(null, null, null); - Exception afterTestClassException = null; + Throwable afterTestClassException = null; // Traverse the TestExecutionListeners in reverse order to ensure proper // "wrapper"-style execution of listeners. for (TestExecutionListener testExecutionListener : getReversedTestExecutionListeners()) { try { testExecutionListener.afterTestClass(getTestContext()); } - catch (Exception ex) { - logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + - "] to process 'after class' callback for test class [" + testClass + "]", ex); + catch (Throwable ex) { + if (logger.isWarnEnabled()) { + logger.warn("Caught exception while allowing TestExecutionListener [" + testExecutionListener + + "] to process 'after class' callback for test class [" + testClass + "]", ex); + } if (afterTestClassException == null) { afterTestClassException = ex; } } } if (afterTestClassException != null) { - throw afterTestClassException; + ReflectionUtils.rethrowException(afterTestClassException); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java index 81d61cb2..fbca021a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java @@ -36,6 +36,8 @@ package org.springframework.test.context; * <ul> * <li>{@link org.springframework.test.context.web.ServletTestExecutionListener * ServletTestExecutionListener}</li> + * <li>{@link org.springframework.test.context.support.DirtiesContextBeforeModesTestExecutionListener + * DirtiesContextBeforeModesTestExecutionListener}</li> * <li>{@link org.springframework.test.context.support.DependencyInjectionTestExecutionListener * DependencyInjectionTestExecutionListener}</li> * <li>{@link org.springframework.test.context.support.DirtiesContextTestExecutionListener diff --git a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java index d99c48d7..d1db5c75 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java @@ -42,10 +42,10 @@ import org.springframework.core.annotation.AliasFor; * @see TestContextManager * @see ContextConfiguration */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface TestExecutionListeners { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java index ec2528af..6937870b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java +++ b/spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java @@ -82,10 +82,10 @@ import org.springframework.core.annotation.AliasFor; * @see org.springframework.core.env.PropertySource * @see org.springframework.context.annotation.PropertySource */ +@Target(ElementType.TYPE) +@Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@Retention(RetentionPolicy.RUNTIME) -@Target(ElementType.TYPE) public @interface TestPropertySource { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java index 27902a6d..1f15c763 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java @@ -26,7 +26,9 @@ import org.springframework.test.context.MergedContextConfiguration; * <em>Spring TestContext Framework</em>. * * <p>A {@code ContextCache} maintains a cache of {@code ApplicationContexts} - * keyed by {@link MergedContextConfiguration} instances. + * keyed by {@link MergedContextConfiguration} instances, potentially configured + * with a {@linkplain ContextCacheUtils#retrieveMaxCacheSize maximum size} and + * a custom eviction policy. * * <h3>Rationale</h3> * <p>Context caching can have significant performance benefits if context @@ -40,6 +42,7 @@ import org.springframework.test.context.MergedContextConfiguration; * @author Sam Brannen * @author Juergen Hoeller * @since 4.2 + * @see ContextCacheUtils#retrieveMaxCacheSize() */ public interface ContextCache { @@ -49,6 +52,25 @@ public interface ContextCache { */ String CONTEXT_CACHE_LOGGING_CATEGORY = "org.springframework.test.context.cache"; + /** + * The default maximum size of the context cache: {@value #DEFAULT_MAX_CONTEXT_CACHE_SIZE}. + * @since 4.3 + * @see #MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME + */ + int DEFAULT_MAX_CONTEXT_CACHE_SIZE = 32; + + /** + * System property used to configure the maximum size of the {@link ContextCache} + * as a positive integer. May alternatively be configured via the + * {@link org.springframework.core.SpringProperties} mechanism. + * <p>Note that implementations of {@code ContextCache} are not required to + * actually support a maximum cache size. Consult the documentation of the + * corresponding implementation for details. + * @since 4.3 + * @see #DEFAULT_MAX_CONTEXT_CACHE_SIZE + */ + String MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME = "spring.test.context.cache.maxSize"; + /** * Determine whether there is a cached context for the given key. diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java new file mode 100644 index 00000000..66427cee --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java @@ -0,0 +1,54 @@ +/* + * 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.cache; + +import org.springframework.core.SpringProperties; +import org.springframework.util.StringUtils; + +/** + * Collection of utilities for working with {@link ContextCache ContextCaches}. + * + * @author Sam Brannen + * @since 4.3 + */ +public abstract class ContextCacheUtils { + + /** + * Retrieve the maximum size of the {@link ContextCache}. + * <p>Uses {@link SpringProperties} to retrieve a system property or Spring + * property named {@code spring.test.context.cache.maxSize}. + * <p>Falls back to the value of the {@link ContextCache#DEFAULT_MAX_CONTEXT_CACHE_SIZE} + * if no such property has been set or if the property is not an integer. + * @return the maximum size of the context cache + * @see ContextCache#MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME + */ + public static int retrieveMaxCacheSize() { + try { + String maxSize = SpringProperties.getProperty(ContextCache.MAX_CONTEXT_CACHE_SIZE_PROPERTY_NAME); + if (StringUtils.hasText(maxSize)) { + return Integer.parseInt(maxSize.trim()); + } + } + catch (Exception ex) { + // ignore + } + + // Fallback + return ContextCache.DEFAULT_MAX_CONTEXT_CACHE_SIZE; + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java index 09678135..d4aaa441 100644 --- a/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java +++ b/spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,7 +17,9 @@ package org.springframework.test.context.cache; import java.util.ArrayList; +import java.util.Collections; import java.util.HashSet; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Set; @@ -37,12 +39,18 @@ import org.springframework.util.Assert; /** * Default implementation of the {@link ContextCache} API. * - * <p>Uses {@link ConcurrentHashMap ConcurrentHashMaps} to cache - * {@link ApplicationContext} and {@link MergedContextConfiguration} instances. + * <p>Uses a synchronized {@link Map} configured with a maximum size + * and a <em>least recently used</em> (LRU) eviction policy to cache + * {@link ApplicationContext} instances. + * + * <p>The maximum size may be supplied as a {@linkplain #DefaultContextCache(int) + * constructor argument} or set via a system property or Spring property named + * {@code spring.test.context.cache.maxSize}. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 + * @see ContextCacheUtils#retrieveMaxCacheSize() */ public class DefaultContextCache implements ContextCache { @@ -52,7 +60,7 @@ public class DefaultContextCache implements ContextCache { * Map of context keys to Spring {@code ApplicationContext} instances. */ private final Map<MergedContextConfiguration, ApplicationContext> contextMap = - new ConcurrentHashMap<MergedContextConfiguration, ApplicationContext>(64); + Collections.synchronizedMap(new LruCache(32, 0.75f)); /** * Map of parent keys to sets of children keys, representing a top-down <em>tree</em> @@ -61,7 +69,9 @@ public class DefaultContextCache implements ContextCache { * of other contexts. */ private final Map<MergedContextConfiguration, Set<MergedContextConfiguration>> hierarchyMap = - new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(64); + new ConcurrentHashMap<MergedContextConfiguration, Set<MergedContextConfiguration>>(32); + + private final int maxSize; private final AtomicInteger hitCount = new AtomicInteger(); @@ -69,6 +79,32 @@ public class DefaultContextCache implements ContextCache { /** + * Create a new {@code DefaultContextCache} using the maximum cache size + * obtained via {@link ContextCacheUtils#retrieveMaxCacheSize()}. + * @since 4.3 + * @see #DefaultContextCache(int) + * @see ContextCacheUtils#retrieveMaxCacheSize() + */ + public DefaultContextCache() { + this(ContextCacheUtils.retrieveMaxCacheSize()); + } + + /** + * Create a new {@code DefaultContextCache} using the supplied maximum + * cache size. + * @param maxSize the maximum cache size + * @throws IllegalArgumentException if the supplied {@code maxSize} value + * is not positive + * @since 4.3 + * @see #DefaultContextCache() + */ + public DefaultContextCache(int maxSize) { + Assert.isTrue(maxSize > 0, "'maxSize' must be positive"); + this.maxSize = maxSize; + } + + + /** * {@inheritDoc} */ @Override @@ -182,6 +218,13 @@ public class DefaultContextCache implements ContextCache { } /** + * Get the maximum size of this cache. + */ + public int getMaxSize() { + return this.maxSize; + } + + /** * {@inheritDoc} */ @Override @@ -210,7 +253,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void reset() { - synchronized (contextMap) { + synchronized (this.contextMap) { clear(); clearStatistics(); } @@ -221,7 +264,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void clear() { - synchronized (contextMap) { + synchronized (this.contextMap) { this.contextMap.clear(); this.hierarchyMap.clear(); } @@ -232,7 +275,7 @@ public class DefaultContextCache implements ContextCache { */ @Override public void clearStatistics() { - synchronized (contextMap) { + synchronized (this.contextMap) { this.hitCount.set(0); this.missCount.set(0); } @@ -259,10 +302,44 @@ public class DefaultContextCache implements ContextCache { public String toString() { return new ToStringCreator(this) .append("size", size()) + .append("maxSize", getMaxSize()) .append("parentContextCount", getParentContextCount()) .append("hitCount", getHitCount()) .append("missCount", getMissCount()) .toString(); } + + /** + * Simple cache implementation based on {@link LinkedHashMap} with a maximum + * size and a <em>least recently used</em> (LRU) eviction policy that + * properly closes application contexts. + * @since 4.3 + */ + @SuppressWarnings("serial") + private class LruCache extends LinkedHashMap<MergedContextConfiguration, ApplicationContext> { + + /** + * Create a new {@code LruCache} with the supplied initial capacity + * and load factor. + * @param initialCapacity the initial capacity + * @param loadFactor the load factor + */ + LruCache(int initialCapacity, float loadFactor) { + super(initialCapacity, loadFactor, true); + } + + @Override + protected boolean removeEldestEntry(Map.Entry<MergedContextConfiguration, ApplicationContext> eldest) { + if (this.size() > DefaultContextCache.this.getMaxSize()) { + // Do NOT delete "DefaultContextCache.this."; otherwise, we accidentally + // invoke java.util.Map.remove(Object, Object). + DefaultContextCache.this.remove(eldest.getKey(), HierarchyMode.CURRENT_LEVEL); + } + + // Return false since we invoke a custom eviction algorithm. + return false; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java index 65fe0a83..5794dde3 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java @@ -50,9 +50,7 @@ import org.springframework.core.annotation.AliasFor; * multiple instances of {@code @Sql}. * * <p>This annotation may be used as a <em>meta-annotation</em> to create custom - * <em>composed annotations</em>; however, attribute overrides are not currently - * supported for {@linkplain Repeatable repeatable} annotations that are used as - * meta-annotations. + * <em>composed annotations</em> with attribute overrides. * * @author Sam Brannen * @since 4.1 diff --git a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java index 71e0e1f6..c58867eb 100644 --- a/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -26,7 +26,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.ApplicationContext; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.core.io.ByteArrayResource; import org.springframework.core.io.ClassPathResource; import org.springframework.core.io.Resource; @@ -129,10 +129,10 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen private void executeSqlScripts(TestContext testContext, ExecutionPhase executionPhase) throws Exception { boolean classLevel = false; - Set<Sql> sqlAnnotations = AnnotationUtils.getRepeatableAnnotations(testContext.getTestMethod(), Sql.class, + Set<Sql> sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(testContext.getTestMethod(), Sql.class, SqlGroup.class); if (sqlAnnotations.isEmpty()) { - sqlAnnotations = AnnotationUtils.getRepeatableAnnotations(testContext.getTestClass(), Sql.class, + sqlAnnotations = AnnotatedElementUtils.getMergedRepeatableAnnotations(testContext.getTestClass(), Sql.class, SqlGroup.class); if (!sqlAnnotations.isEmpty()) { classLevel = true; @@ -157,7 +157,6 @@ public class SqlScriptsTestExecutionListener extends AbstractTestExecutionListen * @param classLevel {@code true} if {@link Sql @Sql} was declared at the * class level */ - @SuppressWarnings("serial") private void executeSqlScripts(Sql sql, ExecutionPhase executionPhase, TestContext testContext, boolean classLevel) throws Exception { if (executionPhase != sql.executionPhase()) { diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java index d16b05f8..474aef2c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -61,16 +61,16 @@ import org.springframework.test.context.web.ServletTestExecutionListener; * <ul> * <li>If you do not wish for your test classes to be tied to a Spring-specific * class hierarchy, you may configure your own custom test classes by using - * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration @ContextConfiguration}, + * {@link SpringRunner}, {@link ContextConfiguration @ContextConfiguration}, * {@link TestExecutionListeners @TestExecutionListeners}, etc.</li> * <li>If you wish to extend this class and use a runner other than the - * {@link SpringJUnit4ClassRunner}, as of Spring Framework 4.2 you can use + * {@link SpringRunner}, as of Spring Framework 4.2 you can use * {@link org.springframework.test.context.junit4.rules.SpringClassRule SpringClassRule} and * {@link org.springframework.test.context.junit4.rules.SpringMethodRule SpringMethodRule} * and specify your runner of choice via {@link RunWith @RunWith(...)}.</li> * </ul> * - * <p><strong>NOTE:</strong> As of Spring Framework 4.1, this class requires JUnit 4.9 or higher. + * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * * @author Sam Brannen * @since 2.5 @@ -85,7 +85,7 @@ import org.springframework.test.context.web.ServletTestExecutionListener; * @see AbstractTransactionalJUnit4SpringContextTests * @see org.springframework.test.context.testng.AbstractTestNGSpringContextTests */ -@RunWith(SpringJUnit4ClassRunner.class) +@RunWith(SpringRunner.class) @TestExecutionListeners({ ServletTestExecutionListener.class, DirtiesContextBeforeModesTestExecutionListener.class, DependencyInjectionTestExecutionListener.class, DirtiesContextTestExecutionListener.class }) public abstract class AbstractJUnit4SpringContextTests implements ApplicationContextAware { diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java index 9745e597..b6c533de 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -64,16 +64,16 @@ import org.springframework.transaction.annotation.Transactional; * <ul> * <li>If you do not wish for your test classes to be tied to a Spring-specific * class hierarchy, you may configure your own custom test classes by using - * {@link SpringJUnit4ClassRunner}, {@link ContextConfiguration @ContextConfiguration}, + * {@link SpringRunner}, {@link ContextConfiguration @ContextConfiguration}, * {@link TestExecutionListeners @TestExecutionListeners}, etc.</li> * <li>If you wish to extend this class and use a runner other than the - * {@link SpringJUnit4ClassRunner}, as of Spring Framework 4.2 you can use + * {@link SpringRunner}, as of Spring Framework 4.2 you can use * {@link org.springframework.test.context.junit4.rules.SpringClassRule SpringClassRule} and * {@link org.springframework.test.context.junit4.rules.SpringMethodRule SpringMethodRule} * and specify your runner of choice via {@link org.junit.runner.RunWith @RunWith(...)}.</li> * </ul> * - * <p><strong>NOTE:</strong> As of Spring Framework 4.1, this class requires JUnit 4.9 or higher. + * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * * @author Sam Brannen * @author Juergen Hoeller @@ -83,7 +83,6 @@ import org.springframework.transaction.annotation.Transactional; * @see org.springframework.test.context.TestExecutionListeners * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener * @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener - * @see org.springframework.test.context.transaction.TransactionConfiguration * @see org.springframework.transaction.annotation.Transactional * @see org.springframework.test.annotation.Commit * @see org.springframework.test.annotation.Rollback diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java index 1d6a11f9..a0ed3db9 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.test.context.junit4; import java.lang.reflect.Field; import java.lang.reflect.Method; +import java.util.concurrent.TimeUnit; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -55,6 +56,9 @@ import org.springframework.util.ReflectionUtils; * <em>Spring TestContext Framework</em> to standard JUnit tests by means of the * {@link TestContextManager} and associated support classes and annotations. * + * <p>To use this class, simply annotate a JUnit 4 based test class with + * {@code @RunWith(SpringJUnit4ClassRunner.class)} or {@code @RunWith(SpringRunner.class)}. + * * <p>The following list constitutes all annotations currently supported directly * or indirectly by {@code SpringJUnit4ClassRunner}. <em>(Note that additional * annotations may be supported by various @@ -75,11 +79,12 @@ import org.springframework.util.ReflectionUtils; * <p>If you would like to use the Spring TestContext Framework with a runner * other than this one, use {@link SpringClassRule} and {@link SpringMethodRule}. * - * <p><strong>NOTE:</strong> As of Spring Framework 4.1, this class requires JUnit 4.9 or higher. + * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * * @author Sam Brannen * @author Juergen Hoeller * @since 2.5 + * @see SpringRunner * @see TestContextManager * @see AbstractJUnit4SpringContextTests * @see AbstractTransactionalJUnit4SpringContextTests @@ -92,27 +97,20 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { private static final Method withRulesMethod; - // Used by RunAfterTestClassCallbacks and RunAfterTestMethodCallbacks - private static final String MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME = "org.junit.runners.model.MultipleFailureException"; - static { - boolean junit4dot9Present = ClassUtils.isPresent(MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME, - SpringJUnit4ClassRunner.class.getClassLoader()); - if (!junit4dot9Present) { - throw new IllegalStateException(String.format( - "Failed to find class [%s]: SpringJUnit4ClassRunner requires JUnit 4.9 or higher.", - MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME)); + if (!ClassUtils.isPresent("org.junit.internal.Throwables", SpringJUnit4ClassRunner.class.getClassLoader())) { + throw new IllegalStateException("SpringJUnit4ClassRunner requires JUnit 4.12 or higher."); } withRulesMethod = ReflectionUtils.findMethod(SpringJUnit4ClassRunner.class, "withRules", FrameworkMethod.class, Object.class, Statement.class); if (withRulesMethod == null) { - throw new IllegalStateException( - "Failed to find withRules() method: SpringJUnit4ClassRunner requires JUnit 4.9 or higher."); + throw new IllegalStateException("SpringJUnit4ClassRunner requires JUnit 4.12 or higher."); } ReflectionUtils.makeAccessible(withRulesMethod); } + private final TestContextManager testContextManager; @@ -376,8 +374,7 @@ public class SpringJUnit4ClassRunner extends BlockJUnit4ClassRunner { statement = new SpringFailOnTimeout(next, springTimeout); } else if (junitTimeout > 0) { - // TODO Use FailOnTimeout.builder() once JUnit 4.12 is the minimum supported version. - statement = new FailOnTimeout(next, junitTimeout); + statement = FailOnTimeout.builder().withTimeout(junitTimeout, TimeUnit.MILLISECONDS).build(next); } else { statement = next; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java new file mode 100644 index 00000000..37dad335 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java @@ -0,0 +1,52 @@ +/* + * 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.junit4; + +import org.junit.runners.model.InitializationError; + +/** + * {@code SpringRunner} is an <em>alias</em> for the {@link SpringJUnit4ClassRunner}. + * + * <p>To use this class, simply annotate a JUnit 4 based test class with + * {@code @RunWith(SpringRunner.class)}. + * + * <p>If you would like to use the Spring TestContext Framework with a runner other than + * this one, use {@link org.springframework.test.context.junit4.rules.SpringClassRule} + * and {@link org.springframework.test.context.junit4.rules.SpringMethodRule}. + * + * <p><strong>NOTE:</strong> This class requires JUnit 4.12 or higher. + * + * @author Sam Brannen + * @since 4.3 + * @see SpringJUnit4ClassRunner + * @see org.springframework.test.context.junit4.rules.SpringClassRule + * @see org.springframework.test.context.junit4.rules.SpringMethodRule + */ +public final class SpringRunner extends SpringJUnit4ClassRunner { + + /** + * Construct a new {@code SpringRunner} and initialize a + * {@link org.springframework.test.context.TestContextManager TestContextManager} + * to provide Spring testing functionality to standard JUnit 4 tests. + * @param clazz the test class to be run + * @see #createTestContextManager(Class) + */ + public SpringRunner(Class<?> clazz) throws InitializationError { + super(clazz); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java index d322cc36..0a93b268 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java @@ -1,4 +1,5 @@ /** - * Support classes for integrating the <em>Spring TestContext Framework</em> with JUnit. + * Support classes for integrating the <em>Spring TestContext Framework</em> + * with JUnit 4.12 or higher. */ package org.springframework.test.context.junit4; diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java index 904a3643..cbbe7437 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java @@ -23,6 +23,7 @@ import java.util.concurrent.ConcurrentHashMap; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.junit.Rule; import org.junit.rules.TestRule; import org.junit.runner.Description; @@ -76,7 +77,7 @@ import org.springframework.util.ClassUtils; * <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li> * </ul> * - * <p><strong>NOTE:</strong> This class requires JUnit 4.9 or higher. + * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * * @author Sam Brannen * @author Philippe Marschall @@ -96,16 +97,9 @@ public class SpringClassRule implements TestRule { private static final Map<Class<?>, TestContextManager> testContextManagerCache = new ConcurrentHashMap<Class<?>, TestContextManager>(64); - // Used by RunAfterTestClassCallbacks - private static final String MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME = "org.junit.runners.model.MultipleFailureException"; - static { - boolean junit4dot9Present = ClassUtils.isPresent(MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME, - SpringClassRule.class.getClassLoader()); - if (!junit4dot9Present) { - throw new IllegalStateException(String.format( - "Failed to find class [%s]: SpringClassRule requires JUnit 4.9 or higher.", - MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME)); + if (!ClassUtils.isPresent("org.junit.internal.Throwables", SpringClassRule.class.getClassLoader())) { + throw new IllegalStateException("SpringClassRule requires JUnit 4.12 or higher."); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java index 7bc200a0..89a97418 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java @@ -20,6 +20,7 @@ import java.lang.reflect.Field; import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; + import org.junit.ClassRule; import org.junit.rules.MethodRule; import org.junit.runners.model.FrameworkMethod; @@ -79,7 +80,7 @@ import org.springframework.util.ReflectionUtils; * <li>{@link org.springframework.test.annotation.IfProfileValue @IfProfileValue}</li> * </ul> * - * <p><strong>NOTE:</strong> This class requires JUnit 4.9 or higher. + * <p><strong>NOTE:</strong> As of Spring Framework 4.3, this class requires JUnit 4.12 or higher. * * @author Sam Brannen * @author Philippe Marschall @@ -93,16 +94,9 @@ public class SpringMethodRule implements MethodRule { private static final Log logger = LogFactory.getLog(SpringMethodRule.class); - // Used by RunAfterTestMethodCallbacks - private static final String MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME = "org.junit.runners.model.MultipleFailureException"; - static { - boolean junit4dot9Present = ClassUtils.isPresent(MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME, - SpringMethodRule.class.getClassLoader()); - if (!junit4dot9Present) { - throw new IllegalStateException(String.format( - "Failed to find class [%s]: SpringMethodRule requires JUnit 4.9 or higher.", - MULTIPLE_FAILURE_EXCEPTION_CLASS_NAME)); + if (!ClassUtils.isPresent("org.junit.internal.Throwables", SpringMethodRule.class.getClassLoader())) { + throw new IllegalStateException("SpringMethodRule requires JUnit 4.12 or higher."); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java index 144c4154..5d53bbff 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -19,10 +19,10 @@ package org.springframework.test.context.junit4.statements; import java.lang.annotation.Annotation; import java.lang.reflect.Method; -import org.junit.Assume; +import org.junit.AssumptionViolatedException; import org.junit.runners.model.Statement; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.annotation.IfProfileValue; import org.springframework.test.annotation.ProfileValueUtils; import org.springframework.util.Assert; @@ -64,6 +64,7 @@ public class ProfileValueChecker extends Statement { this.testMethod = testMethod; } + /** * Determine if the test specified by arguments to the * {@linkplain #ProfileValueChecker constructor} is <em>enabled</em> in @@ -76,27 +77,24 @@ public class ProfileValueChecker extends Statement { * will simply evaluate the next {@link Statement} in the execution chain. * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Class) * @see ProfileValueUtils#isTestEnabledInThisEnvironment(Method, Class) - * @see org.junit.Assume + * @throws AssumptionViolatedException if the test is disabled + * @throws Throwable if evaluation of the next statement fails */ @Override public void evaluate() throws Throwable { if (this.testMethod == null) { if (!ProfileValueUtils.isTestEnabledInThisEnvironment(this.testClass)) { - // Invoke assumeTrue() with false to avoid direct reference to JUnit's - // AssumptionViolatedException which exists in two packages as of JUnit 4.12. - Annotation ann = AnnotationUtils.findAnnotation(this.testClass, IfProfileValue.class); - Assume.assumeTrue(String.format( + Annotation ann = AnnotatedElementUtils.findMergedAnnotation(this.testClass, IfProfileValue.class); + throw new AssumptionViolatedException(String.format( "Profile configured via [%s] is not enabled in this environment for test class [%s].", - ann, this.testClass.getName()), false); + ann, this.testClass.getName())); } } else { if (!ProfileValueUtils.isTestEnabledInThisEnvironment(this.testMethod, this.testClass)) { - // Invoke assumeTrue() with false to avoid direct reference to JUnit's - // AssumptionViolatedException which exists in two packages as of JUnit 4.12. - Assume.assumeTrue(String.format( + throw new AssumptionViolatedException(String.format( "Profile configured via @IfProfileValue is not enabled in this environment for test method [%s].", - this.testMethod), false); + this.testMethod)); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java index e7946dd6..bf05536a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java @@ -80,13 +80,7 @@ public class RunAfterTestClassCallbacks extends Statement { errors.add(ex); } - if (errors.isEmpty()) { - return; - } - if (errors.size() == 1) { - throw errors.get(0); - } - throw new MultipleFailureException(errors); + MultipleFailureException.assertEmpty(errors); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java index 1a1aa877..e0771264 100644 --- a/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java +++ b/spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java @@ -97,13 +97,7 @@ public class RunAfterTestMethodCallbacks extends Statement { errors.add(ex); } - if (errors.isEmpty()) { - return; - } - if (errors.size() == 1) { - throw errors.get(0); - } - throw new MultipleFailureException(errors); + MultipleFailureException.assertEmpty(errors); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java index 6994c798..f17782f8 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java @@ -33,6 +33,7 @@ import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.env.PropertySource; import org.springframework.core.io.ClassPathResource; import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; @@ -56,10 +57,13 @@ import org.springframework.util.ResourceUtils; * * @author Sam Brannen * @author Juergen Hoeller + * @author Phillip Webb * @since 2.5 * @see #generateDefaultLocations * @see #getResourceSuffixes * @see #modifyLocations + * @see #prepareContext + * @see #customizeContext */ public abstract class AbstractContextLoader implements SmartContextLoader { @@ -110,12 +114,13 @@ public abstract class AbstractContextLoader implements SmartContextLoader { * <li>Determines what (if any) context initializer classes have been supplied * via the {@code MergedContextConfiguration} and instantiates and * {@linkplain ApplicationContextInitializer#initialize invokes} each with the - * given application context.</li> + * given application context. * <ul> * <li>Any {@code ApplicationContextInitializers} implementing * {@link org.springframework.core.Ordered Ordered} or annotated with {@link * org.springframework.core.annotation.Order @Order} will be sorted appropriately.</li> * </ul> + * </li> * </ul> * @param context the newly created application context * @param mergedConfig the merged context configuration @@ -166,6 +171,23 @@ public abstract class AbstractContextLoader implements SmartContextLoader { } } + /** + * Customize the {@link ConfigurableApplicationContext} created by this + * {@code ContextLoader} <em>after</em> bean definitions have been loaded + * into the context but <em>before</em> the context has been refreshed. + * <p>The default implementation delegates to all + * {@link MergedContextConfiguration#getContextCustomizers context customizers} + * that have been registered with the supplied {@code mergedConfig}. + * @param context the newly created application context + * @param mergedConfig the merged context configuration + * @since 4.3 + */ + protected void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + for (ContextCustomizer contextCustomizer : mergedConfig.getContextCustomizers()) { + contextCustomizer.customizeContext(context, mergedConfig); + } + } + // --- ContextLoader ------------------------------------------------------- diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java index ca60c8dd..c0ae608e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -29,7 +29,6 @@ import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; -import org.springframework.util.ObjectUtils; /** * {@code AbstractDelegatingSmartContextLoader} serves as an abstract base class @@ -202,15 +201,6 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte name(getAnnotationConfigLoader()), configAttributes)); } - // If neither loader detected defaults and no initializers were declared, - // throw an exception. - if (!configAttributes.hasResources() && ObjectUtils.isEmpty(configAttributes.getInitializers())) { - throw new IllegalStateException(String.format( - "Neither %s nor %s was able to detect defaults, and no ApplicationContextInitializers " - + "were declared for context configuration %s", name(getXmlLoader()), - name(getAnnotationConfigLoader()), configAttributes)); - } - if (configAttributes.hasLocations() && configAttributes.hasClasses()) { String message = String.format( "Configuration error: both default locations AND default configuration classes " @@ -263,8 +253,9 @@ public abstract class AbstractDelegatingSmartContextLoader implements SmartConte } // If neither of the candidates supports the mergedConfig based on resources but - // ACIs were declared, then delegate to the annotation config loader. - if (!mergedConfig.getContextInitializerClasses().isEmpty()) { + // ACIs or customizers were declared, then delegate to the annotation config + // loader. + if (!mergedConfig.getContextInitializerClasses().isEmpty() || !mergedConfig.getContextCustomizers().isEmpty()) { return delegateLoading(getAnnotationConfigLoader(), mergedConfig); } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java index 41e42fcf..4bc65d29 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java @@ -16,7 +16,6 @@ package org.springframework.test.context.support; - import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; @@ -53,6 +52,7 @@ import org.springframework.util.StringUtils; * * @author Sam Brannen * @author Juergen Hoeller + * @author Phillip Webb * @since 2.5 * @see #loadContext(MergedContextConfiguration) * @see #loadContext(String...) @@ -92,6 +92,8 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * annotation configuration processors.</li> * <li>Calls {@link #customizeContext(GenericApplicationContext)} to allow for customizing the context * before it is refreshed.</li> + * <li>Calls {@link #customizeContext(ConfigurableApplicationContext, MergedContextConfiguration)} to + * allow for customizing the context before it is refreshed.</li> * <li>{@link ConfigurableApplicationContext#refresh Refreshes} the * context and registers a JVM shutdown hook for it.</li> * </ul> @@ -122,6 +124,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader loadBeanDefinitions(context, mergedConfig); AnnotationConfigUtils.registerAnnotationConfigProcessors(context); customizeContext(context); + customizeContext(context, mergedConfig); context.refresh(); context.registerShutdownHook(); return context; @@ -205,6 +208,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * @see GenericApplicationContext#setAllowBeanDefinitionOverriding * @see GenericApplicationContext#setResourceLoader * @see GenericApplicationContext#setId + * @see #prepareContext(ConfigurableApplicationContext, MergedContextConfiguration) * @since 2.5 */ protected void prepareContext(GenericApplicationContext context) { @@ -278,6 +282,7 @@ public abstract class AbstractGenericContextLoader extends AbstractContextLoader * @param context the newly created application context * @see #loadContext(MergedContextConfiguration) * @see #loadContext(String...) + * @see #customizeContext(ConfigurableApplicationContext, MergedContextConfiguration) * @since 2.5 */ protected void customizeContext(GenericApplicationContext context) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java index 7049115c..fb9e28c0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java @@ -18,6 +18,7 @@ package org.springframework.test.context.support; import java.util.ArrayList; import java.util.Arrays; +import java.util.Collection; import java.util.Collections; import java.util.HashSet; import java.util.LinkedHashSet; @@ -30,8 +31,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.beans.BeanInstantiationException; import org.springframework.beans.BeanUtils; -import org.springframework.context.ApplicationContextInitializer; -import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.annotation.AnnotationAwareOrderComparator; import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.io.support.SpringFactoriesLoader; @@ -39,6 +38,8 @@ import org.springframework.test.context.BootstrapContext; import org.springframework.test.context.CacheAwareContextLoaderDelegate; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfigurationAttributes; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.ContextCustomizerFactory; import org.springframework.test.context.ContextHierarchy; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; @@ -71,6 +72,7 @@ import org.springframework.util.StringUtils; * * @author Sam Brannen * @author Juergen Hoeller + * @author Phillip Webb * @since 4.1 */ public abstract class AbstractTestContextBootstrapper implements TestContextBootstrapper { @@ -272,11 +274,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot if (MetaAnnotationUtils.findAnnotationDescriptorForTypes( testClass, ContextConfiguration.class, ContextHierarchy.class) == null) { - if (logger.isInfoEnabled()) { - logger.info(String.format("Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s]", - testClass.getName())); - } - return new MergedContextConfiguration(testClass, null, null, null, null); + return buildDefaultMergedContextConfiguration(testClass, cacheAwareContextLoaderDelegate); } if (AnnotationUtils.findAnnotation(testClass, ContextHierarchy.class) != null) { @@ -296,7 +294,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot Class<?> declaringClass = reversedList.get(0).getDeclaringClass(); mergedConfig = buildMergedContextConfiguration( - declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate); + declaringClass, reversedList, parentConfig, cacheAwareContextLoaderDelegate, true); parentConfig = mergedConfig; } @@ -306,8 +304,24 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot else { return buildMergedContextConfiguration(testClass, ContextLoaderUtils.resolveContextConfigurationAttributes(testClass), - null, cacheAwareContextLoaderDelegate); + null, cacheAwareContextLoaderDelegate, true); + } + } + + private MergedContextConfiguration buildDefaultMergedContextConfiguration(Class<?> testClass, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + + List<ContextConfigurationAttributes> defaultConfigAttributesList = + Collections.singletonList(new ContextConfigurationAttributes(testClass)); + + ContextLoader contextLoader = resolveContextLoader(testClass, defaultConfigAttributesList); + if (logger.isInfoEnabled()) { + logger.info(String.format( + "Neither @ContextConfiguration nor @ContextHierarchy found for test class [%s], using %s", + testClass.getName(), contextLoader.getClass().getSimpleName())); } + return buildMergedContextConfiguration(testClass, defaultConfigAttributesList, null, + cacheAwareContextLoaderDelegate, false); } /** @@ -323,6 +337,9 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot * context in a context hierarchy, or {@code null} if there is no parent * @param cacheAwareContextLoaderDelegate the cache-aware context loader delegate to * be passed to the {@code MergedContextConfiguration} constructor + * @param requireLocationsClassesOrInitializers whether locations, classes, or + * initializers are required; typically {@code true} but may be set to {@code false} + * if the configured loader supports empty configuration * @return the merged context configuration * @see #resolveContextLoader * @see ContextLoaderUtils#resolveContextConfigurationAttributes @@ -334,48 +351,90 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot */ private MergedContextConfiguration buildMergedContextConfiguration(Class<?> testClass, List<ContextConfigurationAttributes> configAttributesList, MergedContextConfiguration parentConfig, - CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate) { + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, + boolean requireLocationsClassesOrInitializers) { + + Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be null or empty"); ContextLoader contextLoader = resolveContextLoader(testClass, configAttributesList); - List<String> locationsList = new ArrayList<String>(); - List<Class<?>> classesList = new ArrayList<Class<?>>(); + List<String> locations = new ArrayList<String>(); + List<Class<?>> classes = new ArrayList<Class<?>>(); + List<Class<?>> initializers = new ArrayList<Class<?>>(); for (ContextConfigurationAttributes configAttributes : configAttributesList) { if (logger.isTraceEnabled()) { logger.trace(String.format("Processing locations and classes for context configuration attributes %s", - configAttributes)); + configAttributes)); } if (contextLoader instanceof SmartContextLoader) { SmartContextLoader smartContextLoader = (SmartContextLoader) contextLoader; smartContextLoader.processContextConfiguration(configAttributes); - locationsList.addAll(0, Arrays.asList(configAttributes.getLocations())); - classesList.addAll(0, Arrays.asList(configAttributes.getClasses())); + locations.addAll(0, Arrays.asList(configAttributes.getLocations())); + classes.addAll(0, Arrays.asList(configAttributes.getClasses())); } else { - String[] processedLocations = contextLoader.processLocations(configAttributes.getDeclaringClass(), - configAttributes.getLocations()); - locationsList.addAll(0, Arrays.asList(processedLocations)); + String[] processedLocations = contextLoader.processLocations( + configAttributes.getDeclaringClass(), configAttributes.getLocations()); + locations.addAll(0, Arrays.asList(processedLocations)); // Legacy ContextLoaders don't know how to process classes } + initializers.addAll(0, Arrays.asList(configAttributes.getInitializers())); if (!configAttributes.isInheritLocations()) { break; } } - String[] locations = StringUtils.toStringArray(locationsList); - Class<?>[] classes = ClassUtils.toClassArray(classesList); - Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> initializerClasses = // - ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList); - String[] activeProfiles = ActiveProfilesUtils.resolveActiveProfiles(testClass); - MergedTestPropertySources mergedTestPropertySources = TestPropertySourceUtils.buildMergedTestPropertySources(testClass); + Set<ContextCustomizer> contextCustomizers = getContextCustomizers(testClass, + Collections.unmodifiableList(configAttributesList)); - MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass, locations, classes, - initializerClasses, activeProfiles, mergedTestPropertySources.getLocations(), - mergedTestPropertySources.getProperties(), contextLoader, cacheAwareContextLoaderDelegate, parentConfig); + if (requireLocationsClassesOrInitializers && + areAllEmpty(locations, classes, initializers, contextCustomizers)) { + throw new IllegalStateException(String.format( + "%s was unable to detect defaults, and no ApplicationContextInitializers " + + "or ContextCustomizers were declared for context configuration attributes %s", + contextLoader.getClass().getSimpleName(), configAttributesList)); + } + + MergedTestPropertySources mergedTestPropertySources = + TestPropertySourceUtils.buildMergedTestPropertySources(testClass); + MergedContextConfiguration mergedConfig = new MergedContextConfiguration(testClass, + StringUtils.toStringArray(locations), + ClassUtils.toClassArray(classes), + ApplicationContextInitializerUtils.resolveInitializerClasses(configAttributesList), + ActiveProfilesUtils.resolveActiveProfiles(testClass), + mergedTestPropertySources.getLocations(), + mergedTestPropertySources.getProperties(), + contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parentConfig); return processMergedContextConfiguration(mergedConfig); } + private Set<ContextCustomizer> getContextCustomizers(Class<?> testClass, + List<ContextConfigurationAttributes> configAttributes) { + + List<ContextCustomizerFactory> factories = getContextCustomizerFactories(); + Set<ContextCustomizer> customizers = new LinkedHashSet<ContextCustomizer>(factories.size()); + for (ContextCustomizerFactory factory : factories) { + ContextCustomizer customizer = factory.createContextCustomizer(testClass, configAttributes); + if (customizer != null) { + customizers.add(customizer); + } + } + return customizers; + } + + /** + * Get the {@link ContextCustomizerFactory} instances for this bootstrapper. + * <p>The default implementation uses the {@link SpringFactoriesLoader} mechanism + * for loading factories configured in all {@code META-INF/spring.factories} + * files on the classpath. + * @since 4.3 + * @see SpringFactoriesLoader#loadFactories + */ + protected List<ContextCustomizerFactory> getContextCustomizerFactories() { + return SpringFactoriesLoader.loadFactories(ContextCustomizerFactory.class, getClass().getClassLoader()); + } + /** * Resolve the {@link ContextLoader} {@linkplain Class class} to use for the * supplied list of {@link ContextConfigurationAttributes} and then instantiate @@ -388,7 +447,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot * @param testClass the test class for which the {@code ContextLoader} should be * resolved; must not be {@code null} * @param configAttributesList the list of configuration attributes to process; must - * not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em> + * not be {@code null}; must be ordered <em>bottom-up</em> * (i.e., as if we were traversing up the class hierarchy) * @return the resolved {@code ContextLoader} for the supplied {@code testClass} * (never {@code null}) @@ -399,7 +458,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot List<ContextConfigurationAttributes> configAttributesList) { Assert.notNull(testClass, "Class must not be null"); - Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null"); Class<? extends ContextLoader> contextLoaderClass = resolveExplicitContextLoaderClass(configAttributesList); if (contextLoaderClass == null) { @@ -428,7 +487,7 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot * step #1.</li> * </ol> * @param configAttributesList the list of configuration attributes to process; - * must not be {@code null} or <em>empty</em>; must be ordered <em>bottom-up</em> + * must not be {@code null}; must be ordered <em>bottom-up</em> * (i.e., as if we were traversing up the class hierarchy) * @return the {@code ContextLoader} class to use for the supplied configuration * attributes, or {@code null} if no explicit loader is found @@ -438,7 +497,8 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot protected Class<? extends ContextLoader> resolveExplicitContextLoaderClass( List<ContextConfigurationAttributes> configAttributesList) { - Assert.notEmpty(configAttributesList, "ContextConfigurationAttributes list must not be empty"); + Assert.notNull(configAttributesList, "ContextConfigurationAttributes list must not be null"); + for (ContextConfigurationAttributes configAttributes : configAttributesList) { if (logger.isTraceEnabled()) { logger.trace(String.format("Resolving ContextLoader for context configuration attributes %s", @@ -497,4 +557,14 @@ public abstract class AbstractTestContextBootstrapper implements TestContextBoot return mergedConfig; } + + private static boolean areAllEmpty(Collection<?>... collections) { + for (Collection<?> collection : collections) { + if (!collection.isEmpty()) { + return false; + } + } + return true; + } + } diff --git a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java index baa15001..968d2300 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -24,7 +24,7 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; import org.springframework.context.annotation.Configuration; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.SmartContextLoader; import org.springframework.util.Assert; @@ -103,7 +103,7 @@ public abstract class AnnotationConfigContextLoaderUtils { */ private static boolean isDefaultConfigurationClassCandidate(Class<?> clazz) { return (clazz != null && isStaticNonPrivateAndNonFinal(clazz) && - (AnnotationUtils.findAnnotation(clazz, Configuration.class) != null)); + AnnotatedElementUtils.hasAnnotation(clazz, Configuration.class)); } private static boolean isStaticNonPrivateAndNonFinal(Class<?> clazz) { diff --git a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java index da3383cb..49819fd5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -81,13 +81,12 @@ abstract class ContextLoaderUtils { * (must not be {@code null}) * @return the list of lists of configuration attributes for the specified class; * never {@code null} - * @throws IllegalArgumentException if the supplied class is {@code null}; if + * @throws IllegalArgumentException if the supplied class is {@code null}; or if * neither {@code @ContextConfiguration} nor {@code @ContextHierarchy} is - * <em>present</em> on the supplied class; or if a test class or composed annotation + * <em>present</em> on the supplied class + * @throws IllegalStateException if a test class or composed annotation * in the class hierarchy declares both {@code @ContextConfiguration} and * {@code @ContextHierarchy} as top-level annotations. - * @throws IllegalStateException if no class in the class hierarchy declares - * {@code @ContextHierarchy}. * @since 3.2.2 * @see #buildContextHierarchyMap(Class) * @see #resolveContextConfigurationAttributes(Class) @@ -95,11 +94,10 @@ abstract class ContextLoaderUtils { @SuppressWarnings("unchecked") static List<List<ContextConfigurationAttributes>> resolveContextHierarchyAttributes(Class<?> testClass) { Assert.notNull(testClass, "Class must not be null"); - Assert.state(findAnnotation(testClass, ContextHierarchy.class) != null, "@ContextHierarchy must be present"); - final Class<ContextConfiguration> contextConfigType = ContextConfiguration.class; - final Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class; - final List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>(); + Class<ContextConfiguration> contextConfigType = ContextConfiguration.class; + Class<ContextHierarchy> contextHierarchyType = ContextHierarchy.class; + List<List<ContextConfigurationAttributes>> hierarchyAttributes = new ArrayList<List<ContextConfigurationAttributes>>(); UntypedAnnotationDescriptor desc = findAnnotationDescriptorForTypes(testClass, contextConfigType, contextHierarchyType); @@ -124,7 +122,7 @@ abstract class ContextLoaderUtils { throw new IllegalStateException(msg); } - final List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>(); + List<ContextConfigurationAttributes> configAttributesList = new ArrayList<ContextConfigurationAttributes>(); if (contextConfigDeclaredLocally) { ContextConfiguration contextConfiguration = AnnotationUtils.synthesizeAnnotation( diff --git a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java index 1619ea84..98e9c936 100644 --- a/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -35,10 +35,11 @@ import org.springframework.core.env.MapPropertySource; import org.springframework.core.env.PropertySource; import org.springframework.core.env.PropertySources; import org.springframework.core.io.Resource; +import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.support.ResourcePropertySource; import org.springframework.test.context.TestPropertySource; import org.springframework.test.context.util.TestContextResourceUtils; -import org.springframework.test.util.MetaAnnotationUtils.AnnotationDescriptor; +import org.springframework.test.util.MetaAnnotationUtils.*; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; import org.springframework.util.StringUtils; @@ -57,19 +58,15 @@ import static org.springframework.test.util.MetaAnnotationUtils.*; */ public abstract class TestPropertySourceUtils { - private static final Log logger = LogFactory.getLog(TestPropertySourceUtils.class); - /** * The name of the {@link MapPropertySource} created from <em>inlined properties</em>. * @since 4.1.5 - * @see {@link #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[])} + * @see #addInlinedPropertiesToEnvironment */ public static final String INLINED_PROPERTIES_PROPERTY_SOURCE_NAME = "Inlined Test Properties"; + private static final Log logger = LogFactory.getLog(TestPropertySourceUtils.class); - private TestPropertySourceUtils() { - /* no-op */ - } static MergedTestPropertySources buildMergedTestPropertySources(Class<?> testClass) { Class<TestPropertySource> annotationType = TestPropertySource.class; @@ -78,7 +75,6 @@ public abstract class TestPropertySourceUtils { return new MergedTestPropertySources(); } - // else... List<TestPropertySourceAttributes> attributesList = resolveTestPropertySourceAttributes(testClass); String[] locations = mergeLocations(attributesList); String[] properties = mergeProperties(attributesList); @@ -87,30 +83,27 @@ public abstract class TestPropertySourceUtils { private static List<TestPropertySourceAttributes> resolveTestPropertySourceAttributes(Class<?> testClass) { Assert.notNull(testClass, "Class must not be null"); + List<TestPropertySourceAttributes> attributesList = new ArrayList<TestPropertySourceAttributes>(); + Class<TestPropertySource> annotationType = TestPropertySource.class; - final List<TestPropertySourceAttributes> attributesList = new ArrayList<TestPropertySourceAttributes>(); - final Class<TestPropertySource> annotationType = TestPropertySource.class; AnnotationDescriptor<TestPropertySource> descriptor = findAnnotationDescriptor(testClass, annotationType); Assert.notNull(descriptor, String.format( - "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", - annotationType.getName(), testClass.getName())); + "Could not find an 'annotation declaring class' for annotation type [%s] and class [%s]", + annotationType.getName(), testClass.getName())); while (descriptor != null) { TestPropertySource testPropertySource = descriptor.synthesizeAnnotation(); Class<?> rootDeclaringClass = descriptor.getRootDeclaringClass(); - if (logger.isTraceEnabled()) { logger.trace(String.format("Retrieved @TestPropertySource [%s] for declaring class [%s].", testPropertySource, rootDeclaringClass.getName())); } - - TestPropertySourceAttributes attributes = new TestPropertySourceAttributes(rootDeclaringClass, - testPropertySource); + TestPropertySourceAttributes attributes = + new TestPropertySourceAttributes(rootDeclaringClass, testPropertySource); if (logger.isTraceEnabled()) { logger.trace("Resolved TestPropertySource attributes: " + attributes); } attributesList.add(attributes); - descriptor = findAnnotationDescriptor(rootDeclaringClass.getSuperclass(), annotationType); } @@ -119,74 +112,90 @@ public abstract class TestPropertySourceUtils { private static String[] mergeLocations(List<TestPropertySourceAttributes> attributesList) { final List<String> locations = new ArrayList<String>(); - for (TestPropertySourceAttributes attrs : attributesList) { if (logger.isTraceEnabled()) { logger.trace(String.format("Processing locations for TestPropertySource attributes %s", attrs)); } - String[] locationsArray = TestContextResourceUtils.convertToClasspathResourcePaths( - attrs.getDeclaringClass(), attrs.getLocations()); + attrs.getDeclaringClass(), attrs.getLocations()); locations.addAll(0, Arrays.<String> asList(locationsArray)); - if (!attrs.isInheritLocations()) { break; } } - return StringUtils.toStringArray(locations); } private static String[] mergeProperties(List<TestPropertySourceAttributes> attributesList) { final List<String> properties = new ArrayList<String>(); - for (TestPropertySourceAttributes attrs : attributesList) { if (logger.isTraceEnabled()) { logger.trace(String.format("Processing inlined properties for TestPropertySource attributes %s", attrs)); } - - properties.addAll(0, Arrays.<String> asList(attrs.getProperties())); - + properties.addAll(0, Arrays.<String>asList(attrs.getProperties())); if (!attrs.isInheritProperties()) { break; } } - return StringUtils.toStringArray(properties); } /** * Add the {@link Properties} files from the given resource {@code locations} * to the {@link Environment} of the supplied {@code context}. + * <p>This method simply delegates to + * {@link #addPropertiesFilesToEnvironment(ConfigurableEnvironment, ResourceLoader, String...)}. + * @param context the application context whose environment should be updated; + * never {@code null} + * @param locations the resource locations of {@code Properties} files to add + * to the environment; potentially empty but never {@code null} + * @since 4.1.5 + * @see ResourcePropertySource + * @see TestPropertySource#locations + * @see #addPropertiesFilesToEnvironment(ConfigurableEnvironment, ResourceLoader, String...) + * @throws IllegalStateException if an error occurs while processing a properties file + */ + public static void addPropertiesFilesToEnvironment(ConfigurableApplicationContext context, String... locations) { + Assert.notNull(context, "'context' must not be null"); + Assert.notNull(locations, "'locations' must not be null"); + addPropertiesFilesToEnvironment(context.getEnvironment(), context, locations); + } + + /** + * Add the {@link Properties} files from the given resource {@code locations} + * to the supplied {@link ConfigurableEnvironment environment}. * <p>Property placeholders in resource locations (i.e., <code>${...}</code>) * will be {@linkplain Environment#resolveRequiredPlaceholders(String) resolved} * against the {@code Environment}. * <p>Each properties file will be converted to a {@link ResourcePropertySource} * that will be added to the {@link PropertySources} of the environment with * highest precedence. - * @param context the application context whose environment should be updated; + * @param environment the environment to update; never {@code null} + * @param resourceLoader the {@code ResourceLoader} to use to load each resource; * never {@code null} * @param locations the resource locations of {@code Properties} files to add * to the environment; potentially empty but never {@code null} - * @since 4.1.5 + * @since 4.3 * @see ResourcePropertySource * @see TestPropertySource#locations + * @see #addPropertiesFilesToEnvironment(ConfigurableApplicationContext, String...) * @throws IllegalStateException if an error occurs while processing a properties file */ - public static void addPropertiesFilesToEnvironment(ConfigurableApplicationContext context, - String[] locations) { - Assert.notNull(context, "context must not be null"); - Assert.notNull(locations, "locations must not be null"); + public static void addPropertiesFilesToEnvironment(ConfigurableEnvironment environment, + ResourceLoader resourceLoader, String... locations) { + + Assert.notNull(environment, "'environment' must not be null"); + Assert.notNull(resourceLoader, "'resourceLoader' must not be null"); + Assert.notNull(locations, "'locations' must not be null"); try { - ConfigurableEnvironment environment = context.getEnvironment(); for (String location : locations) { String resolvedLocation = environment.resolveRequiredPlaceholders(location); - Resource resource = context.getResource(resolvedLocation); + Resource resource = resourceLoader.getResource(resolvedLocation); environment.getPropertySources().addFirst(new ResourcePropertySource(resource)); } } - catch (IOException e) { - throw new IllegalStateException("Failed to add PropertySource to Environment", e); + catch (IOException ex) { + throw new IllegalStateException("Failed to add PropertySource to Environment", ex); } } @@ -203,10 +212,9 @@ public abstract class TestPropertySourceUtils { * @see TestPropertySource#properties * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[]) */ - public static void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context, - String[] inlinedProperties) { - Assert.notNull(context, "context must not be null"); - Assert.notNull(inlinedProperties, "inlinedProperties must not be null"); + public static void addInlinedPropertiesToEnvironment(ConfigurableApplicationContext context, String... inlinedProperties) { + Assert.notNull(context, "'context' must not be null"); + Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null"); addInlinedPropertiesToEnvironment(context.getEnvironment(), inlinedProperties); } @@ -226,17 +234,22 @@ public abstract class TestPropertySourceUtils { * @see TestPropertySource#properties * @see #convertInlinedPropertiesToMap */ - public static void addInlinedPropertiesToEnvironment(ConfigurableEnvironment environment, String[] inlinedProperties) { - Assert.notNull(environment, "environment must not be null"); - Assert.notNull(inlinedProperties, "inlinedProperties must not be null"); + public static void addInlinedPropertiesToEnvironment(ConfigurableEnvironment environment, String... inlinedProperties) { + Assert.notNull(environment, "'environment' must not be null"); + Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null"); if (!ObjectUtils.isEmpty(inlinedProperties)) { if (logger.isDebugEnabled()) { - logger.debug("Adding inlined properties to environment: " - + ObjectUtils.nullSafeToString(inlinedProperties)); + logger.debug("Adding inlined properties to environment: " + + ObjectUtils.nullSafeToString(inlinedProperties)); + } + MapPropertySource ps = (MapPropertySource) + environment.getPropertySources().get(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME); + if (ps == null) { + ps = new MapPropertySource(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME, + new LinkedHashMap<String, Object>()); + environment.getPropertySources().addFirst(ps); } - MapPropertySource ps = new MapPropertySource(INLINED_PROPERTIES_PROPERTY_SOURCE_NAME, - convertInlinedPropertiesToMap(inlinedProperties)); - environment.getPropertySources().addFirst(ps); + ps.getSource().putAll(convertInlinedPropertiesToMap(inlinedProperties)); } } @@ -257,24 +270,22 @@ public abstract class TestPropertySourceUtils { * a given inlined property contains multiple key-value pairs * @see #addInlinedPropertiesToEnvironment(ConfigurableEnvironment, String[]) */ - public static Map<String, Object> convertInlinedPropertiesToMap(String[] inlinedProperties) { - Assert.notNull(inlinedProperties, "inlinedProperties must not be null"); + public static Map<String, Object> convertInlinedPropertiesToMap(String... inlinedProperties) { + Assert.notNull(inlinedProperties, "'inlinedProperties' must not be null"); Map<String, Object> map = new LinkedHashMap<String, Object>(); - Properties props = new Properties(); + for (String pair : inlinedProperties) { if (!StringUtils.hasText(pair)) { continue; } - try { props.load(new StringReader(pair)); } - catch (Exception e) { - throw new IllegalStateException("Failed to load test environment property from [" + pair + "].", e); + catch (Exception ex) { + throw new IllegalStateException("Failed to load test environment property from [" + pair + "]", ex); } - Assert.state(props.size() == 1, "Failed to load exactly one test environment property from [" + pair + "]."); - + Assert.state(props.size() == 1, "Failed to load exactly one test environment property from [" + pair + "]"); for (String name : props.stringPropertyNames()) { map.put(name, props.getProperty(name)); } diff --git a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java index 1fd08e32..0899e13b 100644 --- a/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java +++ b/spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -67,7 +67,6 @@ import org.springframework.transaction.annotation.Transactional; * @see org.springframework.test.context.TestExecutionListeners * @see org.springframework.test.context.transaction.TransactionalTestExecutionListener * @see org.springframework.test.context.jdbc.SqlScriptsTestExecutionListener - * @see org.springframework.test.context.transaction.TransactionConfiguration * @see org.springframework.transaction.annotation.Transactional * @see org.springframework.test.annotation.Commit * @see org.springframework.test.annotation.Rollback diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java index 04ebe481..a7f6653e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * <p>Test annotation to indicate that the annotated {@code public void} method + * <p>Test annotation which indicates that the annotated {@code void} method * should be executed <em>after</em> a transaction is ended for a test method - * configured to run within a transaction via the {@code @Transactional} annotation. + * configured to run within a transaction via Spring's {@code @Transactional} + * annotation. * - * <p>The {@code @AfterTransaction} methods of superclasses will be executed - * after those of the current class. + * <p>As of Spring Framework 4.3, {@code @AfterTransaction} may be declared on + * Java 8 based interface default methods. + * + * <p>{@code @AfterTransaction} methods declared in superclasses or as interface + * default methods will be executed after those of the current test class. * * <p>As of Spring Framework 4.0, this annotation may be used as a * <em>meta-annotation</em> to create custom <em>composed annotations</em>. diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java index b7110015..217c7d17 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -23,12 +23,16 @@ import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; /** - * <p>Test annotation to indicate that the annotated {@code public void} method + * <p>Test annotation which indicates that the annotated {@code void} method * should be executed <em>before</em> a transaction is started for a test method - * configured to run within a transaction via the {@code @Transactional} annotation. + * configured to run within a transaction via Spring's {@code @Transactional} + * annotation. * - * <p>The {@code @BeforeTransaction} methods of superclasses will be executed - * before those of the current class. + * <p>As of Spring Framework 4.3, {@code @BeforeTransaction} may be declared on + * Java 8 based interface default methods. + * + * <p>{@code @BeforeTransaction} methods declared in superclasses or as interface + * default methods will be executed before those of the current test class. * * <p>As of Spring Framework 4.0, this annotation may be used as a * <em>meta-annotation</em> to create custom <em>composed annotations</em>. diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java index 3b65d690..b00a9955 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,6 +17,7 @@ package org.springframework.test.context.transaction; import java.util.Map; + import javax.sql.DataSource; import org.apache.commons.logging.Log; @@ -73,7 +74,8 @@ public abstract class TestContextTransactionUtils { * <li>Look up the {@code DataSource} by type and name, if the supplied * {@code name} is non-empty, throwing a {@link BeansException} if the named * {@code DataSource} does not exist. - * <li>Attempt to look up a single {@code DataSource} by type. + * <li>Attempt to look up the single {@code DataSource} by type. + * <li>Attempt to look up the <em>primary</em> {@code DataSource} by type. * <li>Attempt to look up the {@code DataSource} by type and the * {@linkplain #DEFAULT_DATA_SOURCE_NAME default data source name}. * @param testContext the test context for which the {@code DataSource} @@ -110,15 +112,21 @@ public abstract class TestContextTransactionUtils { if (dataSources.size() == 1) { return dataSources.values().iterator().next(); } + + try { + // look up single bean by type, with support for 'primary' beans + return bf.getBean(DataSource.class); + } + catch (BeansException ex) { + logBeansException(testContext, ex, PlatformTransactionManager.class); + } } // look up by type and default name return bf.getBean(DEFAULT_DATA_SOURCE_NAME, DataSource.class); } catch (BeansException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Caught exception while retrieving DataSource for test context " + testContext, ex); - } + logBeansException(testContext, ex, DataSource.class); return null; } } @@ -133,7 +141,8 @@ public abstract class TestContextTransactionUtils { * <li>Look up the transaction manager by type and explicit name, if the supplied * {@code name} is non-empty, throwing a {@link BeansException} if the named * transaction manager does not exist. - * <li>Attempt to look up the transaction manager by type. + * <li>Attempt to look up the single transaction manager by type. + * <li>Attempt to look up the <em>primary</em> transaction manager by type. * <li>Attempt to look up the transaction manager via a * {@link TransactionManagementConfigurer}, if present. * <li>Attempt to look up the transaction manager by type and the @@ -176,6 +185,14 @@ public abstract class TestContextTransactionUtils { return txMgrs.values().iterator().next(); } + try { + // look up single bean by type, with support for 'primary' beans + return bf.getBean(PlatformTransactionManager.class); + } + catch (BeansException ex) { + logBeansException(testContext, ex, PlatformTransactionManager.class); + } + // look up single TransactionManagementConfigurer Map<String, TransactionManagementConfigurer> configurers = BeanFactoryUtils.beansOfTypeIncludingAncestors( lbf, TransactionManagementConfigurer.class); @@ -192,14 +209,18 @@ public abstract class TestContextTransactionUtils { return bf.getBean(DEFAULT_TRANSACTION_MANAGER_NAME, PlatformTransactionManager.class); } catch (BeansException ex) { - if (logger.isDebugEnabled()) { - logger.debug("Caught exception while retrieving transaction manager for test context " + testContext, - ex); - } + logBeansException(testContext, ex, PlatformTransactionManager.class); return null; } } + private static void logBeansException(TestContext testContext, BeansException ex, Class<?> beanType) { + if (logger.isDebugEnabled()) { + logger.debug(String.format("Caught exception while retrieving %s for test context %s", + beanType.getSimpleName(), testContext), ex); + } + } + /** * Create a delegating {@link TransactionAttribute} for the supplied target * {@link TransactionAttribute} and {@link TestContext}, using the names of diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java index 98857e5a..e35e862e 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -40,8 +40,9 @@ import java.lang.annotation.Target; * @see org.springframework.test.context.jdbc.SqlConfig * @see org.springframework.test.context.jdbc.SqlConfig#transactionManager * @see org.springframework.test.context.ContextConfiguration - * @deprecated As of Spring Framework 4.2, use {@code @Rollback} at the class - * level and the {@code transactionManager} qualifier in {@code @Transactional}. + * @deprecated As of Spring Framework 4.2, use {@code @Rollback} or + * {@code @Commit} at the class level and the {@code transactionManager} + * qualifier in {@code @Transactional}. */ @Deprecated @Documented diff --git a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java index 6ae8d4bd..356eb3d0 100644 --- a/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -30,6 +30,8 @@ import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.annotation.BeanFactoryAnnotationUtils; import org.springframework.core.annotation.AnnotatedElementUtils; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.test.annotation.Commit; import org.springframework.test.annotation.Rollback; import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; @@ -43,8 +45,6 @@ import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import static org.springframework.core.annotation.AnnotationUtils.*; - /** * {@code TestExecutionListener} that provides support for executing tests * within <em>test-managed transactions</em> by honoring Spring's @@ -83,8 +83,9 @@ import static org.springframework.core.annotation.AnnotationUtils.*; * <h3>Declarative Rollback and Commit Behavior</h3> * <p>By default, test transactions will be automatically <em>rolled back</em> * after completion of the test; however, transactional commit and rollback - * behavior can be configured declaratively via the {@link Rollback @Rollback} - * annotation at the class level and at the method level. + * behavior can be configured declaratively via the {@link Commit @Commit} + * and {@link Rollback @Rollback} annotations at the class level and at the + * method level. * * <h3>Programmatic Transaction Management</h3> * <p>As of Spring Framework 4.1, it is possible to interact with test-managed @@ -96,9 +97,10 @@ import static org.springframework.core.annotation.AnnotationUtils.*; * <p>When executing transactional tests, it is sometimes useful to be able to * execute certain <em>set up</em> or <em>tear down</em> code outside of a * transaction. {@code TransactionalTestExecutionListener} provides such - * support for methods annotated with - * {@link BeforeTransaction @BeforeTransaction} or - * {@link AfterTransaction @AfterTransaction}. + * support for methods annotated with {@link BeforeTransaction @BeforeTransaction} + * or {@link AfterTransaction @AfterTransaction}. As of Spring Framework 4.3, + * {@code @BeforeTransaction} and {@code @AfterTransaction} may also be declared + * on Java 8 based interface default methods. * * <h3>Configuring a Transaction Manager</h3> * <p>{@code TransactionalTestExecutionListener} expects a @@ -133,7 +135,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis @SuppressWarnings("deprecation") private static final TransactionConfigurationAttributes defaultTxConfigAttributes = new TransactionConfigurationAttributes(); - protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(); + // Do not require @Transactional test methods to be public. + protected final TransactionAttributeSource attributeSource = new AnnotationTransactionAttributeSource(false); @SuppressWarnings("deprecation") private TransactionConfigurationAttributes configurationAttributes; @@ -177,8 +180,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis transactionAttribute); if (logger.isDebugEnabled()) { - logger.debug("Explicit transaction definition [" + transactionAttribute + "] found for test context " - + testContext); + logger.debug("Explicit transaction definition [" + transactionAttribute + "] found for test context " + + testContext); } if (transactionAttribute.getPropagationBehavior() == TransactionDefinition.PROPAGATION_NOT_SUPPORTED) { @@ -189,8 +192,8 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis if (tm == null) { throw new IllegalStateException(String.format( - "Failed to retrieve PlatformTransactionManager for @Transactional test for test context %s.", - testContext)); + "Failed to retrieve PlatformTransactionManager for @Transactional test for test context %s.", + testContext)); } } @@ -246,12 +249,15 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis if (logger.isDebugEnabled()) { logger.debug("Executing @BeforeTransaction method [" + method + "] for test context " + testContext); } + ReflectionUtils.makeAccessible(method); method.invoke(testContext.getTestInstance()); } } catch (InvocationTargetException ex) { - logger.error("Exception encountered while executing @BeforeTransaction methods for test context " - + testContext + ".", ex.getTargetException()); + if (logger.isErrorEnabled()) { + logger.error("Exception encountered while executing @BeforeTransaction methods for test context " + + testContext + ".", ex.getTargetException()); + } ReflectionUtils.rethrowException(ex.getTargetException()); } } @@ -273,6 +279,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis if (logger.isDebugEnabled()) { logger.debug("Executing @AfterTransaction method [" + method + "] for test context " + testContext); } + ReflectionUtils.makeAccessible(method); method.invoke(testContext.getTestInstance()); } catch (InvocationTargetException ex) { @@ -280,15 +287,15 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis if (afterTransactionException == null) { afterTransactionException = targetException; } - logger.error("Exception encountered while executing @AfterTransaction method [" + method - + "] for test context " + testContext, targetException); + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context " + testContext, targetException); } catch (Exception ex) { if (afterTransactionException == null) { afterTransactionException = ex; } - logger.error("Exception encountered while executing @AfterTransaction method [" + method - + "] for test context " + testContext, ex); + logger.error("Exception encountered while executing @AfterTransaction method [" + method + + "] for test context " + testContext, ex); } } @@ -311,20 +318,18 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis * @see #getTransactionManager(TestContext) */ protected PlatformTransactionManager getTransactionManager(TestContext testContext, String qualifier) { - // look up by type and qualifier from @Transactional + // Look up by type and qualifier from @Transactional if (StringUtils.hasText(qualifier)) { try { - // Use autowire-capable factory in order to support extended qualifier - // matching (only exposed on the internal BeanFactory, not on the - // ApplicationContext). + // Use autowire-capable factory in order to support extended qualifier matching + // (only exposed on the internal BeanFactory, not on the ApplicationContext). BeanFactory bf = testContext.getApplicationContext().getAutowireCapableBeanFactory(); return BeanFactoryAnnotationUtils.qualifiedBeanOfType(bf, PlatformTransactionManager.class, qualifier); } catch (RuntimeException ex) { if (logger.isWarnEnabled()) { - logger.warn( - String.format( + logger.warn(String.format( "Caught exception while retrieving transaction manager with qualifier '%s' for test context %s", qualifier, testContext), ex); } @@ -359,7 +364,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis /** * Determine whether or not to rollback transactions by default for the * supplied {@linkplain TestContext test context}. - * <p>Supports {@link Rollback @Rollback} or + * <p>Supports {@link Rollback @Rollback}, {@link Commit @Commit}, or * {@link TransactionConfiguration @TransactionConfiguration} at the * class-level. * @param testContext the test context for which the default rollback flag @@ -370,7 +375,7 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis @SuppressWarnings("deprecation") protected final boolean isDefaultRollback(TestContext testContext) throws Exception { Class<?> testClass = testContext.getTestClass(); - Rollback rollback = findAnnotation(testClass, Rollback.class); + Rollback rollback = AnnotatedElementUtils.findMergedAnnotation(testClass, Rollback.class); boolean rollbackPresent = (rollback != null); TransactionConfigurationAttributes txConfigAttributes = retrieveConfigurationAttributes(testContext); @@ -405,111 +410,45 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis */ protected final boolean isRollback(TestContext testContext) throws Exception { boolean rollback = isDefaultRollback(testContext); - Rollback rollbackAnnotation = findAnnotation(testContext.getTestMethod(), Rollback.class); + Rollback rollbackAnnotation = + AnnotatedElementUtils.findMergedAnnotation(testContext.getTestMethod(), Rollback.class); if (rollbackAnnotation != null) { boolean rollbackOverride = rollbackAnnotation.value(); if (logger.isDebugEnabled()) { logger.debug(String.format( - "Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.", - rollbackOverride, rollback, testContext)); + "Method-level @Rollback(%s) overrides default rollback [%s] for test context %s.", + rollbackOverride, rollback, testContext)); } rollback = rollbackOverride; } else { if (logger.isDebugEnabled()) { logger.debug(String.format( - "No method-level @Rollback override: using default rollback [%s] for test context %s.", rollback, - testContext)); + "No method-level @Rollback override: using default rollback [%s] for test context %s.", + rollback, testContext)); } } return rollback; } /** - * Gets all superclasses of the supplied {@link Class class}, including the - * class itself. The ordering of the returned list will begin with the - * supplied class and continue up the class hierarchy, excluding {@link Object}. - * <p>Note: This code has been borrowed from - * {@link org.junit.internal.runners.TestClass#getSuperClasses(Class)} and - * adapted. - * @param clazz the class for which to retrieve the superclasses - * @return all superclasses of the supplied class, excluding {@code Object} - */ - private List<Class<?>> getSuperClasses(Class<?> clazz) { - List<Class<?>> results = new ArrayList<Class<?>>(); - Class<?> current = clazz; - while (current != null && Object.class != current) { - results.add(current); - current = current.getSuperclass(); - } - return results; - } - - /** - * Gets all methods in the supplied {@link Class class} and its superclasses + * Get all methods in the supplied {@link Class class} and its superclasses * which are annotated with the supplied {@code annotationType} but * which are not <em>shadowed</em> by methods overridden in subclasses. - * <p>Note: This code has been borrowed from - * {@link org.junit.internal.runners.TestClass#getAnnotatedMethods(Class)} - * and adapted. + * <p>Default methods on interfaces are also detected. * @param clazz the class for which to retrieve the annotated methods * @param annotationType the annotation type for which to search * @return all annotated methods in the supplied class and its superclasses + * as well as annotated interface default methods */ private List<Method> getAnnotatedMethods(Class<?> clazz, Class<? extends Annotation> annotationType) { - List<Method> results = new ArrayList<Method>(); - for (Class<?> current : getSuperClasses(clazz)) { - for (Method method : current.getDeclaredMethods()) { - Annotation annotation = getAnnotation(method, annotationType); - if (annotation != null && !isShadowed(method, results)) { - results.add(method); - } + List<Method> methods = new ArrayList<Method>(4); + for (Method method : ReflectionUtils.getUniqueDeclaredMethods(clazz)) { + if (AnnotationUtils.getAnnotation(method, annotationType) != null) { + methods.add(method); } } - return results; - } - - /** - * Determine if the supplied {@link Method method} is <em>shadowed</em> by - * a method in the supplied {@link List list} of previous methods. - * <p>Note: This code has been borrowed from - * {@link org.junit.internal.runners.TestClass#isShadowed(Method, List)}. - * @param method the method to check for shadowing - * @param previousMethods the list of methods which have previously been processed - * @return {@code true} if the supplied method is shadowed by a - * method in the {@code previousMethods} list - */ - private boolean isShadowed(Method method, List<Method> previousMethods) { - for (Method each : previousMethods) { - if (isShadowed(method, each)) { - return true; - } - } - return false; - } - - /** - * Determine if the supplied {@linkplain Method current method} is - * <em>shadowed</em> by a {@linkplain Method previous method}. - * <p>Note: This code has been borrowed from - * {@link org.junit.internal.runners.TestClass#isShadowed(Method, Method)}. - * @param current the current method - * @param previous the previous method - * @return {@code true} if the previous method shadows the current one - */ - private boolean isShadowed(Method current, Method previous) { - if (!previous.getName().equals(current.getName())) { - return false; - } - if (previous.getParameterTypes().length != current.getParameterTypes().length) { - return false; - } - for (int i = 0; i < previous.getParameterTypes().length; i++) { - if (!previous.getParameterTypes()[i].equals(current.getParameterTypes()[i])) { - return false; - } - } - return true; + return methods; } /** @@ -531,19 +470,18 @@ public class TransactionalTestExecutionListener extends AbstractTestExecutionLis if (this.configurationAttributes == null) { Class<?> clazz = testContext.getTestClass(); - TransactionConfiguration txConfig = AnnotatedElementUtils.findMergedAnnotation(clazz, - TransactionConfiguration.class); + TransactionConfiguration txConfig = + AnnotatedElementUtils.findMergedAnnotation(clazz, TransactionConfiguration.class); if (logger.isDebugEnabled()) { logger.debug(String.format("Retrieved @TransactionConfiguration [%s] for test class [%s].", - txConfig, clazz.getName())); + txConfig, clazz.getName())); } - TransactionConfigurationAttributes configAttributes = (txConfig == null ? defaultTxConfigAttributes - : new TransactionConfigurationAttributes(txConfig.transactionManager(), txConfig.defaultRollback())); - + TransactionConfigurationAttributes configAttributes = (txConfig == null ? defaultTxConfigAttributes : + new TransactionConfigurationAttributes(txConfig.transactionManager(), txConfig.defaultRollback())); if (logger.isDebugEnabled()) { logger.debug(String.format("Using TransactionConfigurationAttributes %s for test class [%s].", - configAttributes, clazz.getName())); + configAttributes, clazz.getName())); } this.configurationAttributes = configAttributes; } diff --git a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java index f6e1ac9e..a74ca6a2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -53,6 +53,7 @@ import org.springframework.web.context.support.GenericWebApplicationContext; * {@link #loadBeanDefinitions}. * * @author Sam Brannen + * @author Phillip Webb * @since 3.2 * @see #loadContext(MergedContextConfiguration) * @see #loadContext(String...) @@ -256,15 +257,17 @@ public abstract class AbstractGenericWebContextLoader extends AbstractContextLoa * loader <i>after</i> bean definitions have been loaded into the context but * <i>before</i> the context is refreshed. * - * <p>The default implementation is empty but can be overridden in subclasses - * to customize the web application context. + * <p>The default implementation simply delegates to + * {@link AbstractContextLoader#customizeContext(ConfigurableApplicationContext, MergedContextConfiguration)}. * * @param context the newly created web application context * @param webMergedConfig the merged context configuration to use to load the * web application context * @see #loadContext(MergedContextConfiguration) + * @see #customizeContext(ConfigurableApplicationContext, MergedContextConfiguration) */ protected void customizeContext(GenericWebApplicationContext context, WebMergedContextConfiguration webMergedConfig) { + super.customizeContext(context, webMergedConfig); } // --- ContextLoader ------------------------------------------------------- diff --git a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java index a1bdb5a2..32e79eb5 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -25,7 +25,7 @@ import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Conventions; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockServletContext; @@ -33,7 +33,6 @@ import org.springframework.test.context.TestContext; import org.springframework.test.context.TestExecutionListener; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; -import org.springframework.util.Assert; import org.springframework.web.context.WebApplicationContext; import org.springframework.web.context.request.RequestAttributes; import org.springframework.web.context.request.RequestContextHolder; @@ -58,9 +57,10 @@ import org.springframework.web.context.request.ServletWebRequest; * <p>Note that {@code ServletTestExecutionListener} is enabled by default but * generally takes no action if the {@linkplain TestContext#getTestClass() test * class} is not annotated with {@link WebAppConfiguration @WebAppConfiguration}. - * See the Javadoc for individual methods in this class for details. + * See the javadocs for individual methods in this class for details. * * @author Sam Brannen + * @author Phillip Webb * @since 3.2 */ public class ServletTestExecutionListener extends AbstractTestExecutionListener { @@ -70,33 +70,42 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener * whether or not the {@code ServletTestExecutionListener} should {@linkplain * RequestContextHolder#resetRequestAttributes() reset} Spring Web's * {@code RequestContextHolder} in {@link #afterTestMethod(TestContext)}. - * * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. */ public static final String RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName( - ServletTestExecutionListener.class, "resetRequestContextHolder"); + ServletTestExecutionListener.class, "resetRequestContextHolder"); /** * Attribute name for a {@link TestContext} attribute which indicates that * {@code ServletTestExecutionListener} has already populated Spring Web's * {@code RequestContextHolder}. - * * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. */ public static final String POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE = Conventions.getQualifiedAttributeName( - ServletTestExecutionListener.class, "populatedRequestContextHolder"); + ServletTestExecutionListener.class, "populatedRequestContextHolder"); /** * Attribute name for a request attribute which indicates that the * {@link MockHttpServletRequest} stored in the {@link RequestAttributes} * in Spring Web's {@link RequestContextHolder} was created by the TestContext * framework. - * * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. * @since 4.2 */ public static final String CREATED_BY_THE_TESTCONTEXT_FRAMEWORK = Conventions.getQualifiedAttributeName( - ServletTestExecutionListener.class, "createdByTheTestContextFramework"); + ServletTestExecutionListener.class, "createdByTheTestContextFramework"); + + /** + * Attribute name for a {@link TestContext} attribute which indicates that that + * the {@code ServletTestExecutionListener} should be activated. When not set to + * {@code true}, activation occurs when the {@linkplain TestContext#getTestClass() + * test class} is annotated with {@link WebAppConfiguration @WebAppConfiguration}. + * <p>Permissible values include {@link Boolean#TRUE} and {@link Boolean#FALSE}. + * @since 4.3 + */ + public static final String ACTIVATE_LISTENER = Conventions.getQualifiedAttributeName( + ServletTestExecutionListener.class, "activateListener"); + private static final Log logger = LogFactory.getLog(ServletTestExecutionListener.class); @@ -114,7 +123,6 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener * callback phase via Spring Web's {@link RequestContextHolder}, but only if * the {@linkplain TestContext#getTestClass() test class} is annotated with * {@link WebAppConfiguration @WebAppConfiguration}. - * * @see TestExecutionListener#prepareTestInstance(TestContext) * @see #setUpRequestContextIfNecessary(TestContext) */ @@ -128,7 +136,6 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener * {@link RequestContextHolder}, but only if the * {@linkplain TestContext#getTestClass() test class} is annotated with * {@link WebAppConfiguration @WebAppConfiguration}. - * * @see TestExecutionListener#beforeTestMethod(TestContext) * @see #setUpRequestContextIfNecessary(TestContext) */ @@ -146,11 +153,9 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener * into the test instance for subsequent tests by setting the * {@link DependencyInjectionTestExecutionListener#REINJECT_DEPENDENCIES_ATTRIBUTE} * in the test context to {@code true}. - * * <p>The {@link #RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} and * {@link #POPULATED_REQUEST_CONTEXT_HOLDER_ATTRIBUTE} will be subsequently * removed from the test context, regardless of their values. - * * @see TestExecutionListener#afterTestMethod(TestContext) */ @Override @@ -167,8 +172,9 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener testContext.removeAttribute(RESET_REQUEST_CONTEXT_HOLDER_ATTRIBUTE); } - private boolean notAnnotatedWithWebAppConfiguration(TestContext testContext) { - return AnnotationUtils.findAnnotation(testContext.getTestClass(), WebAppConfiguration.class) == null; + private boolean isActivated(TestContext testContext) { + return (Boolean.TRUE.equals(testContext.getAttribute(ACTIVATE_LISTENER)) || + AnnotatedElementUtils.hasAnnotation(testContext.getTestClass(), WebAppConfiguration.class)); } private boolean alreadyPopulatedRequestContextHolder(TestContext testContext) { @@ -176,7 +182,7 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener } private void setUpRequestContextIfNecessary(TestContext testContext) { - if (notAnnotatedWithWebAppConfiguration(testContext) || alreadyPopulatedRequestContextHolder(testContext)) { + if (!isActivated(testContext) || alreadyPopulatedRequestContextHolder(testContext)) { return; } @@ -185,14 +191,16 @@ public class ServletTestExecutionListener extends AbstractTestExecutionListener if (context instanceof WebApplicationContext) { WebApplicationContext wac = (WebApplicationContext) context; ServletContext servletContext = wac.getServletContext(); - Assert.state(servletContext instanceof MockServletContext, String.format( - "The WebApplicationContext for test context %s must be configured with a MockServletContext.", - testContext)); + if (!(servletContext instanceof MockServletContext)) { + throw new IllegalStateException(String.format( + "The WebApplicationContext for test context %s must be configured with a MockServletContext.", + testContext)); + } if (logger.isDebugEnabled()) { logger.debug(String.format( - "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.", - testContext)); + "Setting up MockHttpServletRequest, MockHttpServletResponse, ServletWebRequest, and RequestContextHolder for test context %s.", + testContext)); } MockServletContext mockServletContext = (MockServletContext) servletContext; diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java index e9d17610..695d5f5a 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java @@ -23,8 +23,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; -import org.springframework.test.context.BootstrapWith; - /** * {@code @WebAppConfiguration} is a class-level annotation that is used to * declare that the {@code ApplicationContext} loaded for an integration test @@ -53,7 +51,6 @@ import org.springframework.test.context.BootstrapWith; @Retention(RetentionPolicy.RUNTIME) @Documented @Inherited -@BootstrapWith(WebTestContextBootstrapper.class) public @interface WebAppConfiguration { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java index cd1187b5..98bbdad2 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java @@ -22,6 +22,7 @@ import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.style.ToStringCreator; import org.springframework.test.context.CacheAwareContextLoaderDelegate; +import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.util.ObjectUtils; @@ -132,13 +133,48 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { String resourceBasePath, ContextLoader contextLoader, CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { + this(testClass, locations, classes, contextInitializerClasses, activeProfiles, propertySourceLocations, + propertySourceProperties, null, resourceBasePath, contextLoader, cacheAwareContextLoaderDelegate, parent); + } + + /** + * Create a new {@code WebMergedContextConfiguration} instance for the + * supplied parameters. + * <p>If a {@code null} value is supplied for {@code locations}, + * {@code classes}, {@code activeProfiles}, {@code propertySourceLocations}, + * or {@code propertySourceProperties} an empty array will be stored instead. + * If a {@code null} value is supplied for {@code contextInitializerClasses} + * or {@code contextCustomizers}, an empty set will be stored instead. + * If an <em>empty</em> value is supplied for the {@code resourceBasePath} + * an empty string will be used. Furthermore, active profiles will be sorted, + * and duplicate profiles will be removed. + * @param testClass the test class for which the configuration was merged + * @param locations the merged context resource locations + * @param classes the merged annotated classes + * @param contextInitializerClasses the merged context initializer classes + * @param activeProfiles the merged active bean definition profiles + * @param propertySourceLocations the merged {@code PropertySource} locations + * @param propertySourceProperties the merged {@code PropertySource} properties + * @param contextCustomizers the context customizers + * @param resourceBasePath the resource path to the root directory of the web application + * @param contextLoader the resolved {@code ContextLoader} + * @param cacheAwareContextLoaderDelegate a cache-aware context loader + * delegate with which to retrieve the parent context + * @param parent the parent configuration or {@code null} if there is no parent + * @since 4.3 + */ + public WebMergedContextConfiguration(Class<?> testClass, String[] locations, Class<?>[] classes, + Set<Class<? extends ApplicationContextInitializer<? extends ConfigurableApplicationContext>>> contextInitializerClasses, + String[] activeProfiles, String[] propertySourceLocations, String[] propertySourceProperties, + Set<ContextCustomizer> contextCustomizers, String resourceBasePath, ContextLoader contextLoader, + CacheAwareContextLoaderDelegate cacheAwareContextLoaderDelegate, MergedContextConfiguration parent) { + super(testClass, locations, classes, contextInitializerClasses, activeProfiles, propertySourceLocations, - propertySourceProperties, contextLoader, cacheAwareContextLoaderDelegate, parent); + propertySourceProperties, contextCustomizers, contextLoader, cacheAwareContextLoaderDelegate, parent); - this.resourceBasePath = !StringUtils.hasText(resourceBasePath) ? "" : resourceBasePath; + this.resourceBasePath = (StringUtils.hasText(resourceBasePath) ? resourceBasePath : ""); } - /** * Get the resource path to the root directory of the web application for the * {@linkplain #getTestClass() test class}, configured via {@code @WebAppConfiguration}. @@ -182,6 +218,7 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { * {@linkplain #getActiveProfiles() active profiles}, * {@linkplain #getPropertySourceLocations() property source locations}, * {@linkplain #getPropertySourceProperties() property source properties}, + * {@linkplain #getContextCustomizers() context customizers}, * {@linkplain #getResourceBasePath() resource base path}, the name of the * {@link #getContextLoader() ContextLoader}, and the * {@linkplain #getParent() parent configuration}. @@ -196,6 +233,7 @@ public class WebMergedContextConfiguration extends MergedContextConfiguration { .append("activeProfiles", ObjectUtils.nullSafeToString(getActiveProfiles())) .append("propertySourceLocations", ObjectUtils.nullSafeToString(getPropertySourceLocations())) .append("propertySourceProperties", ObjectUtils.nullSafeToString(getPropertySourceProperties())) + .append("contextCustomizers", getContextCustomizers()) .append("resourceBasePath", getResourceBasePath()) .append("contextLoader", nullSafeToString(getContextLoader())) .append("parent", getParent()) diff --git a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java index 94741f5a..f779450c 100644 --- a/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java +++ b/spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -16,7 +16,7 @@ package org.springframework.test.context.web; -import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.core.annotation.AnnotatedElementUtils; import org.springframework.test.context.ContextLoader; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextBootstrapper; @@ -45,12 +45,12 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { */ @Override protected Class<? extends ContextLoader> getDefaultContextLoaderClass(Class<?> testClass) { - if (AnnotationUtils.findAnnotation(testClass, WebAppConfiguration.class) != null) { + if (AnnotatedElementUtils.findMergedAnnotation(testClass, WebAppConfiguration.class) != null) { return WebDelegatingSmartContextLoader.class; } - - // else... - return super.getDefaultContextLoaderClass(testClass); + else { + return super.getDefaultContextLoaderClass(testClass); + } } /** @@ -61,14 +61,14 @@ public class WebTestContextBootstrapper extends DefaultTestContextBootstrapper { */ @Override protected MergedContextConfiguration processMergedContextConfiguration(MergedContextConfiguration mergedConfig) { - WebAppConfiguration webAppConfiguration = AnnotationUtils.findAnnotation(mergedConfig.getTestClass(), - WebAppConfiguration.class); + WebAppConfiguration webAppConfiguration = + AnnotatedElementUtils.findMergedAnnotation(mergedConfig.getTestClass(), WebAppConfiguration.class); if (webAppConfiguration != null) { return new WebMergedContextConfiguration(mergedConfig, webAppConfiguration.value()); } - - // else... - return mergedConfig; + else { + return mergedConfig; + } } } diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainer.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainer.java new file mode 100644 index 00000000..bd7c7257 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainer.java @@ -0,0 +1,135 @@ +/* + * 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.web.socket; + +import java.io.IOException; +import java.net.URI; +import java.util.Collections; +import java.util.Set; +import javax.websocket.ClientEndpointConfig; +import javax.websocket.DeploymentException; +import javax.websocket.Endpoint; +import javax.websocket.Extension; +import javax.websocket.Session; +import javax.websocket.server.ServerContainer; +import javax.websocket.server.ServerEndpointConfig; + +/** + * Mock implementation of the {@link javax.websocket.server.ServerContainer} interface. + * + * @author Sam Brannen + * @since 4.3.1 + */ +class MockServerContainer implements ServerContainer { + + private long defaultAsyncSendTimeout; + + private long defaultMaxSessionIdleTimeout; + + private int defaultMaxBinaryMessageBufferSize; + + private int defaultMaxTextMessageBufferSize; + + + // --- WebSocketContainer -------------------------------------------------- + + @Override + public long getDefaultAsyncSendTimeout() { + return this.defaultAsyncSendTimeout; + } + + @Override + public void setAsyncSendTimeout(long timeout) { + this.defaultAsyncSendTimeout = timeout; + } + + @Override + public long getDefaultMaxSessionIdleTimeout() { + return this.defaultMaxSessionIdleTimeout; + } + + @Override + public void setDefaultMaxSessionIdleTimeout(long timeout) { + this.defaultMaxSessionIdleTimeout = timeout; + } + + @Override + public int getDefaultMaxBinaryMessageBufferSize() { + return this.defaultMaxBinaryMessageBufferSize; + } + + @Override + public void setDefaultMaxBinaryMessageBufferSize(int max) { + this.defaultMaxBinaryMessageBufferSize = max; + } + + @Override + public int getDefaultMaxTextMessageBufferSize() { + return this.defaultMaxTextMessageBufferSize; + } + + @Override + public void setDefaultMaxTextMessageBufferSize(int max) { + this.defaultMaxTextMessageBufferSize = max; + } + + @Override + public Set<Extension> getInstalledExtensions() { + return Collections.emptySet(); + } + + @Override + public Session connectToServer(Object annotatedEndpointInstance, URI path) throws DeploymentException, IOException { + throw new UnsupportedOperationException("MockServerContainer does not support connectToServer(Object, URI)"); + } + + @Override + public Session connectToServer(Class<?> annotatedEndpointClass, URI path) throws DeploymentException, IOException { + throw new UnsupportedOperationException("MockServerContainer does not support connectToServer(Class, URI)"); + } + + @Override + public Session connectToServer(Endpoint endpointInstance, ClientEndpointConfig cec, URI path) + throws DeploymentException, IOException { + + throw new UnsupportedOperationException( + "MockServerContainer does not support connectToServer(Endpoint, ClientEndpointConfig, URI)"); + } + + @Override + public Session connectToServer(Class<? extends Endpoint> endpointClass, ClientEndpointConfig cec, URI path) + throws DeploymentException, IOException { + + throw new UnsupportedOperationException( + "MockServerContainer does not support connectToServer(Class, ClientEndpointConfig, URI)"); + } + + + // --- ServerContainer ----------------------------------------------------- + + @Override + public void addEndpoint(Class<?> endpointClass) throws DeploymentException { + throw new UnsupportedOperationException("MockServerContainer does not support addEndpoint(Class)"); + } + + @Override + public void addEndpoint(ServerEndpointConfig serverConfig) throws DeploymentException { + throw new UnsupportedOperationException( + "MockServerContainer does not support addEndpoint(ServerEndpointConfig)"); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java new file mode 100644 index 00000000..e68914ee --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java @@ -0,0 +1,52 @@ +/* + * 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.web.socket; + +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.test.context.ContextCustomizer; +import org.springframework.test.context.MergedContextConfiguration; +import org.springframework.web.context.WebApplicationContext; + +/** + * {@link ContextCustomizer} that instantiates a new {@link MockServerContainer} + * and stores it in the {@code ServletContext} under the attribute named + * {@code "javax.websocket.server.ServerContainer"}. + * + * @author Sam Brannen + * @since 4.3.1 + */ +class MockServerContainerContextCustomizer implements ContextCustomizer { + + @Override + public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { + if (context instanceof WebApplicationContext) { + WebApplicationContext wac = (WebApplicationContext) context; + wac.getServletContext().setAttribute("javax.websocket.server.ServerContainer", new MockServerContainer()); + } + } + + @Override + public boolean equals(Object other) { + return (this == other || (other != null && getClass() == other.getClass())); + } + + @Override + public int hashCode() { + return getClass().hashCode(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java new file mode 100644 index 00000000..c76959c3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java @@ -0,0 +1,73 @@ +/* + * 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.web.socket; + +import java.util.List; + +import org.springframework.beans.BeanUtils; +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.ClassUtils; + +/** + * {@link ContextCustomizerFactory} which creates a {@link MockServerContainerContextCustomizer} + * if WebSocket support is present in the classpath and the test class is annotated + * with {@code @WebAppConfiguration}. + * + * @author Sam Brannen + * @since 4.3.1 + */ +class MockServerContainerContextCustomizerFactory implements ContextCustomizerFactory { + + private static final String WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME = + "org.springframework.test.context.web.WebAppConfiguration"; + + private static final String MOCK_SERVER_CONTAINER_CONTEXT_CUSTOMIZER_CLASS_NAME = + "org.springframework.test.context.web.socket.MockServerContainerContextCustomizer"; + + private static final boolean webSocketPresent = ClassUtils.isPresent("javax.websocket.server.ServerContainer", + MockServerContainerContextCustomizerFactory.class.getClassLoader()); + + + @Override + public ContextCustomizer createContextCustomizer(Class<?> testClass, + List<ContextConfigurationAttributes> configAttributes) { + + if (webSocketPresent && isAnnotatedWithWebAppConfiguration(testClass)) { + try { + Class<?> clazz = ClassUtils.forName(MOCK_SERVER_CONTAINER_CONTEXT_CUSTOMIZER_CLASS_NAME, + getClass().getClassLoader()); + return (ContextCustomizer) BeanUtils.instantiateClass(clazz); + } + catch (Throwable ex) { + throw new IllegalStateException("Failed to enable WebSocket test support; could not load class: " + + MOCK_SERVER_CONTAINER_CONTEXT_CUSTOMIZER_CLASS_NAME, ex); + } + } + + // Else, nothing to customize + return null; + } + + private static boolean isAnnotatedWithWebAppConfiguration(Class<?> testClass) { + return (AnnotatedElementUtils.findMergedAnnotationAttributes(testClass, + WEB_APP_CONFIGURATION_ANNOTATION_CLASS_NAME, false, false) != null); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java b/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java new file mode 100644 index 00000000..7b97278f --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java @@ -0,0 +1,4 @@ +/** + * WebSocket support classes for the <em>Spring TestContext Framework</em>. + */ +package org.springframework.test.context.web.socket; diff --git a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java index 8e3e9a05..e0e1bc0e 100644 --- a/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -124,6 +124,7 @@ public class JdbcTestUtils { */ public static int deleteFromTableWhere(JdbcTemplate jdbcTemplate, String tableName, String whereClause, Object... args) { + String sql = "DELETE FROM " + tableName; if (StringUtils.hasText(whereClause)) { sql += " WHERE " + whereClause; @@ -170,6 +171,7 @@ public class JdbcTestUtils { @Deprecated public static void executeSqlScript(JdbcTemplate jdbcTemplate, ResourceLoader resourceLoader, String sqlResourcePath, boolean continueOnError) throws DataAccessException { + Resource resource = resourceLoader.getResource(sqlResourcePath); executeSqlScript(jdbcTemplate, resource, continueOnError); } @@ -197,6 +199,7 @@ public class JdbcTestUtils { @Deprecated public static void executeSqlScript(JdbcTemplate jdbcTemplate, Resource resource, boolean continueOnError) throws DataAccessException { + executeSqlScript(jdbcTemplate, new EncodedResource(resource), continueOnError); } @@ -220,6 +223,7 @@ public class JdbcTestUtils { @Deprecated public static void executeSqlScript(JdbcTemplate jdbcTemplate, EncodedResource resource, boolean continueOnError) throws DataAccessException { + new ResourceDatabasePopulator(continueOnError, false, resource.getEncoding(), resource.getResource()).execute(jdbcTemplate.getDataSource()); } @@ -288,4 +292,5 @@ public class JdbcTestUtils { public static void splitSqlScript(String script, char delim, List<String> statements) { ScriptUtils.splitSqlScript(script, delim, statements); } + } diff --git a/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java index 4eda8a45..7660ee2c 100644 --- a/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -32,6 +32,7 @@ import org.springframework.util.Assert; * @since 4.2 * @see org.springframework.aop.support.AopUtils * @see org.springframework.aop.framework.AopProxyUtils + * @see ReflectionTestUtils */ public class AopTestUtils { diff --git a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java index 0302686b..f07574e8 100644 --- a/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java +++ b/spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.test.util; import org.springframework.util.ObjectUtils; @@ -26,13 +27,8 @@ import org.springframework.util.ObjectUtils; */ public abstract class AssertionErrors { - - private AssertionErrors() { - } - /** * Fails a test with the given message. - * * @param message describes the reason for the failure */ public static void fail(String message) { @@ -42,7 +38,6 @@ public abstract class AssertionErrors { /** * Fails a test with the given message passing along expected and actual * values to be added to the message. - * * <p>For example given: * <pre class="code"> * assertEquals("Response header [" + name + "]", actual, expected); @@ -51,7 +46,6 @@ public abstract class AssertionErrors { * <pre class="code"> * Response header [Accept] expected:<application/json> but was:<text/plain> * </pre> - * * @param message describes the value that failed the match * @param expected expected value * @param actual actual value @@ -63,7 +57,6 @@ public abstract class AssertionErrors { /** * Assert the given condition is {@code true} and raise an * {@link AssertionError} if it is not. - * * @param message the message * @param condition the condition to test for */ @@ -79,14 +72,13 @@ public abstract class AssertionErrors { * <pre class="code"> * assertEquals("Response header [" + name + "]", actual, expected); * </pre> - * * @param message describes the value being checked * @param expected the expected value * @param actual the actual value */ public static void assertEquals(String message, Object expected, Object actual) { if (!ObjectUtils.nullSafeEquals(expected, actual)) { - fail(message, expected, actual); + fail(message, ObjectUtils.nullSafeToString(expected), ObjectUtils.nullSafeToString(actual)); } } diff --git a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java index a82b8a40..cbb74dc6 100644 --- a/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java +++ b/spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java @@ -16,27 +16,21 @@ package org.springframework.test.util; -import java.lang.reflect.Array; -import java.lang.reflect.Method; import java.text.ParseException; import java.util.List; import java.util.Map; +import com.jayway.jsonpath.InvalidPathException; +import com.jayway.jsonpath.JsonPath; import org.hamcrest.Matcher; import org.springframework.util.Assert; import org.springframework.util.ObjectUtils; -import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; -import com.jayway.jsonpath.InvalidPathException; -import com.jayway.jsonpath.JsonPath; - -import static org.hamcrest.MatcherAssert.assertThat; -import static org.hamcrest.core.IsInstanceOf.instanceOf; -import static org.springframework.test.util.AssertionErrors.assertEquals; -import static org.springframework.test.util.AssertionErrors.assertTrue; -import static org.springframework.test.util.AssertionErrors.fail; +import static org.hamcrest.MatcherAssert.*; +import static org.hamcrest.core.IsInstanceOf.*; +import static org.springframework.test.util.AssertionErrors.*; /** * A helper class for applying assertions via JSON path expressions. @@ -52,26 +46,6 @@ import static org.springframework.test.util.AssertionErrors.fail; */ public class JsonPathExpectationsHelper { - private static Method compileMethod; - - private static Object emptyFilters; - - static { - // Reflective bridging between JsonPath 0.9.x and 1.x - for (Method candidate : JsonPath.class.getMethods()) { - if (candidate.getName().equals("compile")) { - Class<?>[] paramTypes = candidate.getParameterTypes(); - if (paramTypes.length == 2 && String.class == paramTypes[0] && paramTypes[1].isArray()) { - compileMethod = candidate; - emptyFilters = Array.newInstance(paramTypes[1].getComponentType(), 0); - break; - } - } - } - Assert.state(compileMethod != null, "Unexpected JsonPath API - no compile(String, ...) method found"); - } - - private final String expression; private final JsonPath jsonPath; @@ -86,8 +60,7 @@ public class JsonPathExpectationsHelper { public JsonPathExpectationsHelper(String expression, Object... args) { Assert.hasText(expression, "expression must not be null or empty"); this.expression = String.format(expression, args); - this.jsonPath = (JsonPath) ReflectionUtils.invokeMethod( - compileMethod, null, this.expression, emptyFilters); + this.jsonPath = JsonPath.compile(this.expression); } diff --git a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java index 913986a4..9c203d74 100644 --- a/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java @@ -57,8 +57,8 @@ public abstract class MetaAnnotationUtils { /** * Find the {@link AnnotationDescriptor} for the supplied {@code annotationType} - * on the supplied {@link Class}, traversing its annotations and superclasses - * if no annotation can be found on the given class itself. + * on the supplied {@link Class}, traversing its annotations, interfaces, and + * superclasses if no annotation can be found on the given class itself. * <p>This method explicitly handles class-level annotations which are not * declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as * well as meta-annotations</em>. @@ -67,14 +67,12 @@ public abstract class MetaAnnotationUtils { * <li>Search for the annotation on the given class and return a corresponding * {@code AnnotationDescriptor} if found. * <li>Recursively search through all annotations that the given class declares. + * <li>Recursively search through all interfaces implemented by the given class. * <li>Recursively search through the superclass hierarchy of the given class. * </ol> * <p>In this context, the term <em>recursively</em> means that the search - * process continues by returning to step #1 with the current annotation or - * superclass as the class to look for annotations on. - * <p>If the supplied {@code clazz} is an interface, only the interface - * itself will be checked; the inheritance hierarchy for interfaces will not - * be traversed. + * process continues by returning to step #1 with the current annotation, + * interface, or superclass as the class to look for annotations on. * @param clazz the class to look for annotations on * @param annotationType the type of annotation to look for * @return the corresponding annotation descriptor if the annotation was found; @@ -123,6 +121,15 @@ public abstract class MetaAnnotationUtils { } } + // Declared on interface? + for (Class<?> ifc : clazz.getInterfaces()) { + AnnotationDescriptor<T> descriptor = findAnnotationDescriptor(ifc, visited, annotationType); + if (descriptor != null) { + return new AnnotationDescriptor<T>(clazz, descriptor.getDeclaringClass(), + descriptor.getComposedAnnotation(), descriptor.getAnnotation()); + } + } + // Declared on a superclass? return findAnnotationDescriptor(clazz.getSuperclass(), visited, annotationType); } @@ -132,8 +139,9 @@ public abstract class MetaAnnotationUtils { * in the inheritance hierarchy of the specified {@code clazz} (including * the specified {@code clazz} itself) which declares at least one of the * specified {@code annotationTypes}. - * <p>This method traverses the annotations and superclasses of the specified - * {@code clazz} if no annotation can be found on the given class itself. + * <p>This method traverses the annotations, interfaces, and superclasses + * of the specified {@code clazz} if no annotation can be found on the given + * class itself. * <p>This method explicitly handles class-level annotations which are not * declared as {@linkplain java.lang.annotation.Inherited inherited} <em>as * well as meta-annotations</em>. @@ -143,14 +151,12 @@ public abstract class MetaAnnotationUtils { * the given class and return a corresponding {@code UntypedAnnotationDescriptor} * if found. * <li>Recursively search through all annotations that the given class declares. + * <li>Recursively search through all interfaces implemented by the given class. * <li>Recursively search through the superclass hierarchy of the given class. * </ol> * <p>In this context, the term <em>recursively</em> means that the search - * process continues by returning to step #1 with the current annotation or - * superclass as the class to look for annotations on. - * <p>If the supplied {@code clazz} is an interface, only the interface - * itself will be checked; the inheritance hierarchy for interfaces will not - * be traversed. + * process continues by returning to step #1 with the current annotation, + * interface, or superclass as the class to look for annotations on. * @param clazz the class to look for annotations on * @param annotationTypes the types of annotations to look for * @return the corresponding annotation descriptor if one of the annotations @@ -203,6 +209,15 @@ public abstract class MetaAnnotationUtils { } } + // Declared on interface? + for (Class<?> ifc : clazz.getInterfaces()) { + UntypedAnnotationDescriptor descriptor = findAnnotationDescriptorForTypes(ifc, visited, annotationTypes); + if (descriptor != null) { + return new UntypedAnnotationDescriptor(clazz, descriptor.getDeclaringClass(), + descriptor.getComposedAnnotation(), descriptor.getAnnotation()); + } + } + // Declared on a superclass? return findAnnotationDescriptorForTypes(clazz.getSuperclass(), visited, annotationTypes); } diff --git a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java index f40227fb..4f508511 100644 --- a/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java +++ b/spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -60,6 +60,7 @@ import org.springframework.util.StringUtils; * @author Juergen Hoeller * @since 2.5 * @see ReflectionUtils + * @see AopTestUtils */ public class ReflectionTestUtils { @@ -137,6 +138,9 @@ public class ReflectionTestUtils { * Set the {@linkplain Field field} with the given {@code name}/{@code type} * on the provided {@code targetObject}/{@code targetClass} to the supplied * {@code value}. + * <p>If the supplied {@code targetObject} is a <em>proxy</em>, it will + * be {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing + * the field to be set on the ultimate target of the proxy. * <p>This method traverses the class hierarchy in search of the desired * field. In addition, an attempt will be made to make non-{@code public} * fields <em>accessible</em>, thus allowing one to set {@code protected}, @@ -150,33 +154,36 @@ public class ReflectionTestUtils { * @param value the value to set * @param type the type of the field to set; may be {@code null} if * {@code name} is specified + * @since 4.2 * @see ReflectionUtils#findField(Class, String, Class) * @see ReflectionUtils#makeAccessible(Field) * @see ReflectionUtils#setField(Field, Object, Object) - * @since 4.2 + * @see AopTestUtils#getUltimateTargetObject(Object) */ public static void setField(Object targetObject, Class<?> targetClass, String name, Object value, Class<?> type) { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); + Object ultimateTarget = (targetObject != null ? AopTestUtils.getUltimateTargetObject(targetObject) : null); + if (targetClass == null) { - targetClass = targetObject.getClass(); + targetClass = ultimateTarget.getClass(); } Field field = ReflectionUtils.findField(targetClass, name, type); if (field == null) { throw new IllegalArgumentException(String.format( - "Could not find field '%s' of type [%s] on target object [%s] or target class [%s]", name, type, - targetObject, targetClass)); + "Could not find field '%s' of type [%s] on %s or target class [%s]", name, type, + safeToString(ultimateTarget), targetClass)); } if (logger.isDebugEnabled()) { logger.debug(String.format( - "Setting field '%s' of type [%s] on target object [%s] or target class [%s] to value [%s]", name, type, - targetObject, targetClass, value)); + "Setting field '%s' of type [%s] on %s or target class [%s] to value [%s]", name, type, + safeToString(ultimateTarget), targetClass, value)); } ReflectionUtils.makeAccessible(field); - ReflectionUtils.setField(field, targetObject, value); + ReflectionUtils.setField(field, ultimateTarget, value); } /** @@ -213,6 +220,9 @@ public class ReflectionTestUtils { /** * Get the value of the {@linkplain Field field} with the given {@code name} * from the provided {@code targetObject}/{@code targetClass}. + * <p>If the supplied {@code targetObject} is a <em>proxy</em>, it will + * be {@linkplain AopTestUtils#getUltimateTargetObject unwrapped} allowing + * the field to be retrieved from the ultimate target of the proxy. * <p>This method traverses the class hierarchy in search of the desired * field. In addition, an attempt will be made to make non-{@code public} * fields <em>accessible</em>, thus allowing one to get {@code protected}, @@ -229,28 +239,30 @@ public class ReflectionTestUtils { * @see ReflectionUtils#findField(Class, String, Class) * @see ReflectionUtils#makeAccessible(Field) * @see ReflectionUtils#getField(Field, Object) + * @see AopTestUtils#getUltimateTargetObject(Object) */ public static Object getField(Object targetObject, Class<?> targetClass, String name) { Assert.isTrue(targetObject != null || targetClass != null, "Either targetObject or targetClass for the field must be specified"); + Object ultimateTarget = (targetObject != null ? AopTestUtils.getUltimateTargetObject(targetObject) : null); + if (targetClass == null) { - targetClass = targetObject.getClass(); + targetClass = ultimateTarget.getClass(); } Field field = ReflectionUtils.findField(targetClass, name); if (field == null) { - throw new IllegalArgumentException( - String.format("Could not find field '%s' on target object [%s] or target class [%s]", name, - targetObject, targetClass)); + throw new IllegalArgumentException(String.format("Could not find field '%s' on %s or target class [%s]", + name, safeToString(ultimateTarget), targetClass)); } if (logger.isDebugEnabled()) { - logger.debug(String.format("Getting field '%s' from target object [%s] or target class [%s]", name, - targetObject, targetClass)); + logger.debug(String.format("Getting field '%s' from %s or target class [%s]", name, + safeToString(ultimateTarget), targetClass)); } ReflectionUtils.makeAccessible(field); - return ReflectionUtils.getField(field, targetObject); + return ReflectionUtils.getField(field, ultimateTarget); } /** @@ -314,13 +326,16 @@ public class ReflectionTestUtils { method = ReflectionUtils.findMethod(target.getClass(), setterMethodName, paramTypes); } if (method == null) { - throw new IllegalArgumentException("Could not find setter method '" + setterMethodName + - "' on target [" + target + "] with parameter type [" + type + "]"); + throw new IllegalArgumentException(String.format( + "Could not find setter method '%s' on %s with parameter type [%s]", setterMethodName, + safeToString(target), type)); } if (logger.isDebugEnabled()) { - logger.debug("Invoking setter method '" + setterMethodName + "' on target [" + target + "]"); + logger.debug(String.format("Invoking setter method '%s' on %s with value [%s]", setterMethodName, + safeToString(target), value)); } + ReflectionUtils.makeAccessible(method); ReflectionUtils.invokeMethod(method, target, value); } @@ -359,12 +374,12 @@ public class ReflectionTestUtils { method = ReflectionUtils.findMethod(target.getClass(), getterMethodName); } if (method == null) { - throw new IllegalArgumentException("Could not find getter method '" + getterMethodName + - "' on target [" + target + "]"); + throw new IllegalArgumentException(String.format( + "Could not find getter method '%s' on %s", getterMethodName, safeToString(target))); } if (logger.isDebugEnabled()) { - logger.debug("Invoking getter method '" + getterMethodName + "' on target [" + target + "]"); + logger.debug(String.format("Invoking getter method '%s' on %s", getterMethodName, safeToString(target))); } ReflectionUtils.makeAccessible(method); return ReflectionUtils.invokeMethod(method, target); @@ -399,8 +414,8 @@ public class ReflectionTestUtils { methodInvoker.prepare(); if (logger.isDebugEnabled()) { - logger.debug("Invoking method '" + name + "' on target [" + target + "] with arguments [" + - ObjectUtils.nullSafeToString(args) + "]"); + logger.debug(String.format("Invoking method '%s' on %s with arguments %s", name, safeToString(target), + ObjectUtils.nullSafeToString(args))); } return (T) methodInvoker.invoke(); @@ -411,4 +426,14 @@ public class ReflectionTestUtils { } } + private static String safeToString(Object target) { + try { + return String.format("target object [%s]", target); + } + catch (Exception ex) { + return String.format("target of type [%s] whose toString() method threw [%s]", + (target != null ? target.getClass().getName() : "unknown"), ex); + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java new file mode 100644 index 00000000..37139cb7 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java @@ -0,0 +1,189 @@ +/* + * 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.web.client; + +import java.io.IOException; +import java.net.URI; +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.LinkedList; +import java.util.List; +import java.util.Set; + +import org.springframework.http.HttpMethod; +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; + +/** + * Base class for {@code RequestExpectationManager} implementations responsible + * for storing expectations and actual requests, and checking for unsatisfied + * expectations at the end. + * + * <p>Subclasses are responsible for validating each request by matching it to + * to expectations following the order of declaration or not. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public abstract class AbstractRequestExpectationManager implements RequestExpectationManager { + + private final List<RequestExpectation> expectations = new LinkedList<RequestExpectation>(); + + private final List<ClientHttpRequest> requests = new LinkedList<ClientHttpRequest>(); + + + protected List<RequestExpectation> getExpectations() { + return this.expectations; + } + + protected List<ClientHttpRequest> getRequests() { + return this.requests; + } + + + @Override + public ResponseActions expectRequest(ExpectedCount count, RequestMatcher matcher) { + Assert.state(getRequests().isEmpty(), "Cannot add more expectations after actual requests are made"); + RequestExpectation expectation = new DefaultRequestExpectation(count, matcher); + getExpectations().add(expectation); + return expectation; + } + + @Override + public ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException { + if (getRequests().isEmpty()) { + afterExpectationsDeclared(); + } + ClientHttpResponse response = validateRequestInternal(request); + getRequests().add(request); + return response; + } + + /** + * Invoked after the phase of declaring expected requests is over. This is + * detected from {@link #validateRequest} on the first actual request. + */ + protected void afterExpectationsDeclared() { + } + + /** + * Subclasses must implement the actual validation of the request + * matching it to a declared expectation. + */ + protected abstract ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException; + + @Override + public void verify() { + if (getExpectations().isEmpty()) { + return; + } + int count = 0; + for (RequestExpectation expectation : getExpectations()) { + if (!expectation.isSatisfied()) { + count++; + } + } + if (count > 0) { + String message = "Further request(s) expected leaving " + count + " unsatisfied expectation(s).\n"; + throw new AssertionError(message + getRequestDetails()); + } + } + + /** + * Return details of executed requests. + */ + protected String getRequestDetails() { + StringBuilder sb = new StringBuilder(); + sb.append(getRequests().size()).append(" request(s) executed"); + if (!getRequests().isEmpty()) { + sb.append(":\n"); + for (ClientHttpRequest request : getRequests()) { + sb.append(request.toString()).append("\n"); + } + } + else { + sb.append(".\n"); + } + return sb.toString(); + } + + /** + * Return an {@code AssertionError} that a sub-class can raise for an + * unexpected request. + */ + protected AssertionError createUnexpectedRequestError(ClientHttpRequest request) { + HttpMethod method = request.getMethod(); + URI uri = request.getURI(); + String message = "No further requests expected: HTTP " + method + " " + uri + "\n"; + return new AssertionError(message + getRequestDetails()); + } + + @Override + public void reset() { + this.expectations.clear(); + this.requests.clear(); + } + + + /** + * Helper class to manage a group of request expectations. It helps with + * operations against the entire group such as finding a match and updating + * (add or remove) based on expected request count. + */ + protected static class RequestExpectationGroup { + + private final Set<RequestExpectation> expectations = new LinkedHashSet<RequestExpectation>(); + + public Set<RequestExpectation> getExpectations() { + return this.expectations; + } + + public void update(RequestExpectation expectation) { + if (expectation.hasRemainingCount()) { + getExpectations().add(expectation); + } + else { + getExpectations().remove(expectation); + } + } + + public void updateAll(Collection<RequestExpectation> expectations) { + for (RequestExpectation expectation : expectations) { + update(expectation); + } + } + + public RequestExpectation findExpectation(ClientHttpRequest request) throws IOException { + for (RequestExpectation expectation : getExpectations()) { + try { + expectation.match(request); + return expectation; + } + catch (AssertionError error) { + // Ignore + } + } + return null; + } + + public void reset() { + this.expectations.clear(); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java new file mode 100644 index 00000000..93a4752a --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java @@ -0,0 +1,146 @@ +/* + * 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.web.client; + +import java.io.IOException; +import java.util.LinkedList; +import java.util.List; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; + +/** + * Default implementation of {@code RequestExpectation} that simply delegates + * to the request matchers and the response creator it contains. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class DefaultRequestExpectation implements RequestExpectation { + + private final RequestCount requestCount; + + private final List<RequestMatcher> requestMatchers = new LinkedList<RequestMatcher>(); + + private ResponseCreator responseCreator; + + + /** + * Create a new request expectation that should be called a number of times + * as indicated by {@code RequestCount}. + * @param expectedCount the expected request expectedCount + */ + public DefaultRequestExpectation(ExpectedCount expectedCount, RequestMatcher requestMatcher) { + Assert.notNull(expectedCount, "'expectedCount' is required"); + Assert.notNull(requestMatcher, "'requestMatcher' is required"); + this.requestCount = new RequestCount(expectedCount); + this.requestMatchers.add(requestMatcher); + } + + + protected RequestCount getRequestCount() { + return this.requestCount; + } + + protected List<RequestMatcher> getRequestMatchers() { + return this.requestMatchers; + } + + protected ResponseCreator getResponseCreator() { + return this.responseCreator; + } + + @Override + public ResponseActions andExpect(RequestMatcher requestMatcher) { + Assert.notNull(requestMatcher, "RequestMatcher is required"); + this.requestMatchers.add(requestMatcher); + return this; + } + + @Override + public void andRespond(ResponseCreator responseCreator) { + Assert.notNull(responseCreator, "ResponseCreator is required"); + this.responseCreator = responseCreator; + } + + @Override + public void match(ClientHttpRequest request) throws IOException { + for (RequestMatcher matcher : getRequestMatchers()) { + matcher.match(request); + } + } + + @Override + public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { + ResponseCreator responseCreator = getResponseCreator(); + if (responseCreator == null) { + throw new IllegalStateException("createResponse called before ResponseCreator was set"); + } + getRequestCount().incrementAndValidate(); + return responseCreator.createResponse(request); + } + + @Override + public boolean hasRemainingCount() { + return getRequestCount().hasRemainingCount(); + } + + @Override + public boolean isSatisfied() { + return getRequestCount().isSatisfied(); + } + + + /** + * Helper class that keeps track of actual vs expected request count. + */ + protected static class RequestCount { + + private final ExpectedCount expectedCount; + + private int matchedRequestCount; + + public RequestCount(ExpectedCount expectedCount) { + this.expectedCount = expectedCount; + } + + public ExpectedCount getExpectedCount() { + return this.expectedCount; + } + + public int getMatchedRequestCount() { + return this.matchedRequestCount; + } + + public void incrementAndValidate() { + this.matchedRequestCount++; + if (getMatchedRequestCount() > getExpectedCount().getMaxCount()) { + throw new AssertionError("No more calls expected."); + } + } + + public boolean hasRemainingCount() { + return (getMatchedRequestCount() < getExpectedCount().getMaxCount()); + } + + public boolean isSatisfied() { + return (getMatchedRequestCount() >= getExpectedCount().getMinCount()); + } + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java b/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java new file mode 100644 index 00000000..09f3077c --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java @@ -0,0 +1,118 @@ +/* + * 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.web.client; + +import org.springframework.util.Assert; + +/** + * A simple type representing a range for an expected count. + * + * <p>Examples: + * <pre> + * import static org.springframework.test.web.client.ExpectedCount.* + * + * once() + * manyTimes() + * times(5) + * min(2) + * max(4) + * between(2, 4) + * </pre> + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class ExpectedCount { + + private final int minCount; + + private final int maxCount; + + + /** + * Private constructor. + * See static factory methods in this class. + */ + private ExpectedCount(int minCount, int maxCount) { + Assert.isTrue(minCount >= 1, "minCount >= 0 is required"); + Assert.isTrue(maxCount >= minCount, "maxCount >= minCount is required"); + this.minCount = minCount; + this.maxCount = maxCount; + } + + + /** + * Return the {@code min} boundary of the expected count range. + */ + public int getMinCount() { + return this.minCount; + } + + /** + * Return the {@code max} boundary of the expected count range. + */ + public int getMaxCount() { + return this.maxCount; + } + + + /** + * Exactly once. + */ + public static ExpectedCount once() { + return new ExpectedCount(1, 1); + } + + /** + * Many times (range of 1..Integer.MAX_VALUE). + */ + public static ExpectedCount manyTimes() { + return new ExpectedCount(1, Integer.MAX_VALUE); + } + + /** + * Exactly N times. + */ + public static ExpectedCount times(int count) { + Assert.isTrue(count >= 1, "'count' must be >= 1"); + return new ExpectedCount(count, count); + } + + /** + * At least {@code min} number of times. + */ + public static ExpectedCount min(int min) { + Assert.isTrue(min >= 1, "'min' must be >= 1"); + return new ExpectedCount(min, Integer.MAX_VALUE); + } + + /** + * At most {@code max} number of times. + */ + public static ExpectedCount max(int max) { + Assert.isTrue(max >= 1, "'max' must be >= 1"); + return new ExpectedCount(1, max); + } + + /** + * Between {@code min} and {@code max} number of times. + */ + public static ExpectedCount between(int min, int max) { + return new ExpectedCount(min, max); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java index 7c7b6115..d1086a53 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,6 +18,7 @@ package org.springframework.test.web.client; import java.io.IOException; import java.net.URI; +import java.nio.charset.Charset; import java.util.List; import org.springframework.http.HttpHeaders; @@ -43,6 +44,8 @@ import static org.springframework.test.web.servlet.request.MockMvcRequestBuilder */ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory { + private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + private final MockMvc mockMvc; @@ -50,6 +53,7 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory this.mockMvc = mockMvc; } + @Override public ClientHttpRequest createRequest(final URI uri, final HttpMethod httpMethod) throws IOException { return new MockClientHttpRequest(httpMethod, uri) { @@ -73,7 +77,7 @@ public class MockMvcClientHttpRequestFactory implements ClientHttpRequestFactory return clientResponse; } catch (Exception ex) { - byte[] body = ex.toString().getBytes("UTF-8"); + byte[] body = ex.toString().getBytes(UTF8_CHARSET); return new MockClientHttpResponse(body, HttpStatus.INTERNAL_SERVER_ERROR); } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java index e1376041..09207f8c 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -18,17 +18,14 @@ package org.springframework.test.web.client; import java.io.IOException; import java.net.URI; -import java.util.Iterator; -import java.util.LinkedList; -import java.util.List; import org.springframework.http.HttpMethod; import org.springframework.http.client.AsyncClientHttpRequest; import org.springframework.http.client.AsyncClientHttpRequestFactory; import org.springframework.http.client.ClientHttpRequest; import org.springframework.http.client.ClientHttpRequestFactory; -import org.springframework.test.web.client.match.MockRestRequestMatchers; -import org.springframework.test.web.client.response.MockRestResponseCreators; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.mock.http.client.MockAsyncClientHttpRequest; import org.springframework.util.Assert; import org.springframework.web.client.AsyncRestTemplate; import org.springframework.web.client.RestTemplate; @@ -36,53 +33,33 @@ import org.springframework.web.client.support.RestGatewaySupport; /** * <strong>Main entry point for client-side REST testing</strong>. Used for tests - * that involve direct or indirect (through client code) use of the - * {@link RestTemplate}. Provides a way to set up fine-grained expectations - * on the requests that will be performed through the {@code RestTemplate} and - * a way to define the responses to send back removing the need for an - * actual running server. + * that involve direct or indirect use of the {@link RestTemplate}. Provides a + * way to set up expected requests that will be performed through the + * {@code RestTemplate} as well as mock responses to send back thus removing the + * need for an actual server. * - * <p>Below is an example: - * <pre class="code"> - * import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; - * import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; - * import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - * - * ... + * <p>Below is an example that assumes static imports from + * {@code MockRestRequestMatchers}, {@code MockRestResponseCreators}, + * and {@code ExpectedCount}: * + * <pre class="code"> * RestTemplate restTemplate = new RestTemplate() - * MockRestServiceServer mockServer = MockRestServiceServer.createServer(restTemplate); + * MockRestServiceServer server = MockRestServiceServer.bindTo(restTemplate).build(); * - * mockServer.expect(requestTo("/hotels/42")).andExpect(method(HttpMethod.GET)) + * server.expect(manyTimes(), requestTo("/hotels/42")).andExpect(method(HttpMethod.GET)) * .andRespond(withSuccess("{ \"id\" : \"42\", \"name\" : \"Holiday Inn\"}", MediaType.APPLICATION_JSON)); * * Hotel hotel = restTemplate.getForObject("/hotels/{id}", Hotel.class, 42); * // Use the hotel instance... * - * mockServer.verify(); + * // Verify all expectations met + * server.verify(); * </pre> * - * <p>To create an instance of this class, use {@link #createServer(RestTemplate)} - * and provide the {@code RestTemplate} to set up for the mock testing. - * - * <p>After that use {@link #expect(RequestMatcher)} and fluent API methods - * {@link ResponseActions#andExpect(RequestMatcher) andExpect(RequestMatcher)} and - * {@link ResponseActions#andRespond(ResponseCreator) andRespond(ResponseCreator)} - * to set up request expectations and responses, most likely relying on the default - * {@code RequestMatcher} implementations provided in {@link MockRestRequestMatchers} - * and the {@code ResponseCreator} implementations provided in - * {@link MockRestResponseCreators} both of which can be statically imported. - * - * <p>At the end of the test use {@link #verify()} to ensure all expected - * requests were actually performed. - * - * <p>Note that because of the fluent API offered by this class (and related - * classes), you can typically use the Code Completion features (i.e. - * ctrl-space) in your IDE to set up the mocks. - * - * <p><strong>Credits:</strong> The client-side REST testing support was - * inspired by and initially based on similar code in the Spring WS project for - * client-side tests involving the {@code WebServiceTemplate}. + * <p>Note that as an alternative to the above you can also set the + * {@link MockMvcClientHttpRequestFactory} on a {@code RestTemplate} which + * allows executing requests against an instance of + * {@link org.springframework.test.web.servlet.MockMvc MockMvc}. * * @author Craig Walls * @author Rossen Stoyanchev @@ -90,143 +67,230 @@ import org.springframework.web.client.support.RestGatewaySupport; */ public class MockRestServiceServer { - private final List<RequestMatcherClientHttpRequest> expectedRequests = - new LinkedList<RequestMatcherClientHttpRequest>(); + private final RequestExpectationManager expectationManager; + + + /** + * Private constructor with {@code RequestExpectationManager}. + * See static builder methods and {@code createServer} shortcut methods. + */ + private MockRestServiceServer(RequestExpectationManager expectationManager) { + this.expectationManager = expectationManager; + } + + + /** + * Set up an expectation for a single HTTP request. The returned + * {@link ResponseActions} can be used to set up further expectations as + * well as to define the response. + * <p>This method may be invoked any number times before starting to make + * request through the underlying {@code RestTemplate} in order to set up + * all expected requests. + * @param matcher request matcher + * @return a representation of the expectation + */ + public ResponseActions expect(RequestMatcher matcher) { + return expect(ExpectedCount.once(), matcher); + } + + /** + * An alternative to {@link #expect(RequestMatcher)} with an indication how + * many times the request is expected to be executed. + * <p>When request expectations have an expected count greater than one, only + * the first execution is expected to match the order of declaration. Subsequent + * request executions may be inserted anywhere thereafter. + * @param count the expected count + * @param matcher request matcher + * @return a representation of the expectation + * @since 4.3 + */ + public ResponseActions expect(ExpectedCount count, RequestMatcher matcher) { + return this.expectationManager.expectRequest(count, matcher); + } + + /** + * Verify that all expected requests set up via + * {@link #expect(RequestMatcher)} were indeed performed. + * @throws AssertionError when some expectations were not met + */ + public void verify() { + this.expectationManager.verify(); + } - private final List<RequestMatcherClientHttpRequest> actualRequests = - new LinkedList<RequestMatcherClientHttpRequest>(); + /** + * Reset the internal state removing all expectations and recorded requests. + */ + public void reset() { + this.expectationManager.reset(); + } + + + /** + * Return a builder for a {@code MockRestServiceServer} that should be used + * to reply to the given {@code RestTemplate}. + * @since 4.3 + */ + public static MockRestServiceServerBuilder bindTo(RestTemplate restTemplate) { + return new DefaultBuilder(restTemplate); + } + /** + * Return a builder for a {@code MockRestServiceServer} that should be used + * to reply to the given {@code AsyncRestTemplate}. + * @since 4.3 + */ + public static MockRestServiceServerBuilder bindTo(AsyncRestTemplate asyncRestTemplate) { + return new DefaultBuilder(asyncRestTemplate); + } /** - * Private constructor. - * @see #createServer(RestTemplate) - * @see #createServer(RestGatewaySupport) + * Return a builder for a {@code MockRestServiceServer} that should be used + * to reply to the given {@code RestGatewaySupport}. + * @since 4.3 */ - private MockRestServiceServer() { + public static MockRestServiceServerBuilder bindTo(RestGatewaySupport restGateway) { + Assert.notNull(restGateway, "'gatewaySupport' must not be null"); + return new DefaultBuilder(restGateway.getRestTemplate()); } /** - * Create a {@code MockRestServiceServer} and set up the given - * {@code RestTemplate} with a mock {@link ClientHttpRequestFactory}. + * A shortcut for {@code bindTo(restTemplate).build()}. * @param restTemplate the RestTemplate to set up for mock testing - * @return the created mock server + * @return the mock server */ public static MockRestServiceServer createServer(RestTemplate restTemplate) { - Assert.notNull(restTemplate, "'restTemplate' must not be null"); - MockRestServiceServer mockServer = new MockRestServiceServer(); - RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory(); - restTemplate.setRequestFactory(factory); - return mockServer; + return bindTo(restTemplate).build(); } /** - * Create a {@code MockRestServiceServer} and set up the given - * {@code AsyRestTemplate} with a mock {@link AsyncClientHttpRequestFactory}. + * A shortcut for {@code bindTo(asyncRestTemplate).build()}. * @param asyncRestTemplate the AsyncRestTemplate to set up for mock testing * @return the created mock server */ public static MockRestServiceServer createServer(AsyncRestTemplate asyncRestTemplate) { - Assert.notNull(asyncRestTemplate, "'asyncRestTemplate' must not be null"); - MockRestServiceServer mockServer = new MockRestServiceServer(); - RequestMatcherClientHttpRequestFactory factory = mockServer.new RequestMatcherClientHttpRequestFactory(); - asyncRestTemplate.setAsyncRequestFactory(factory); - return mockServer; + return bindTo(asyncRestTemplate).build(); } /** - * Create a {@code MockRestServiceServer} and set up the given - * {@code RestGatewaySupport} with a mock {@link ClientHttpRequestFactory}. + * A shortcut for {@code bindTo(restGateway).build()}. * @param restGateway the REST gateway to set up for mock testing * @return the created mock server */ public static MockRestServiceServer createServer(RestGatewaySupport restGateway) { - Assert.notNull(restGateway, "'gatewaySupport' must not be null"); - return createServer(restGateway.getRestTemplate()); + return bindTo(restGateway).build(); } /** - * Set up a new HTTP request expectation. The returned {@link ResponseActions} - * is used to set up further expectations and to define the response. - * <p>This method may be invoked multiple times before starting the test, i.e. before - * using the {@code RestTemplate}, to set up expectations for multiple requests. - * @param requestMatcher a request expectation, see {@link MockRestRequestMatchers} - * @return used to set up further expectations or to define a response + * Builder to create a {@code MockRestServiceServer}. */ - public ResponseActions expect(RequestMatcher requestMatcher) { - Assert.state(this.actualRequests.isEmpty(), "Can't add more expected requests with test already underway"); - RequestMatcherClientHttpRequest request = new RequestMatcherClientHttpRequest(requestMatcher); - this.expectedRequests.add(request); - return request; + public interface MockRestServiceServerBuilder { + + /** + * Whether to allow expected requests to be executed in any order not + * necessarily matching the order of declaration. + * <p>When set to "true" this is effectively a shortcut for:<br> + * {@code builder.build(new UnorderedRequestExpectationManager)}. + * @param ignoreExpectOrder whether to ignore the order of expectations + */ + MockRestServiceServerBuilder ignoreExpectOrder(boolean ignoreExpectOrder); + + /** + * Build the {@code MockRestServiceServer} and set up the underlying + * {@code RestTemplate} or {@code AsyncRestTemplate} with a + * {@link ClientHttpRequestFactory} that creates mock requests. + */ + MockRestServiceServer build(); + + /** + * An overloaded build alternative that accepts a custom + * {@link RequestExpectationManager}. + */ + MockRestServiceServer build(RequestExpectationManager manager); } - /** - * Verify that all expected requests set up via - * {@link #expect(RequestMatcher)} were indeed performed. - * @throws AssertionError when some expectations were not met - */ - public void verify() { - if (this.expectedRequests.isEmpty() || this.expectedRequests.equals(this.actualRequests)) { - return; + + private static class DefaultBuilder implements MockRestServiceServerBuilder { + + private final RestTemplate restTemplate; + + private final AsyncRestTemplate asyncRestTemplate; + + private boolean ignoreExpectOrder; + + public DefaultBuilder(RestTemplate restTemplate) { + Assert.notNull(restTemplate, "RestTemplate must not be null"); + this.restTemplate = restTemplate; + this.asyncRestTemplate = null; + } + + public DefaultBuilder(AsyncRestTemplate asyncRestTemplate) { + Assert.notNull(asyncRestTemplate, "AsyncRestTemplate must not be null"); + this.restTemplate = null; + this.asyncRestTemplate = asyncRestTemplate; + } + + @Override + public MockRestServiceServerBuilder ignoreExpectOrder(boolean ignoreExpectOrder) { + this.ignoreExpectOrder = ignoreExpectOrder; + return this; } - throw new AssertionError(getVerifyMessage()); - } - private String getVerifyMessage() { - StringBuilder sb = new StringBuilder("Further request(s) expected\n"); - if (this.actualRequests.size() > 0) { - sb.append("The following "); + @Override + public MockRestServiceServer build() { + if (this.ignoreExpectOrder) { + return build(new UnorderedRequestExpectationManager()); + } + else { + return build(new SimpleRequestExpectationManager()); + } } - sb.append(this.actualRequests.size()).append(" out of "); - sb.append(this.expectedRequests.size()).append(" were executed"); - if (this.actualRequests.size() > 0) { - sb.append(":\n"); - for (RequestMatcherClientHttpRequest request : this.actualRequests) { - sb.append(request.toString()).append("\n"); + @Override + public MockRestServiceServer build(RequestExpectationManager manager) { + MockRestServiceServer server = new MockRestServiceServer(manager); + MockClientHttpRequestFactory factory = server.new MockClientHttpRequestFactory(); + if (this.restTemplate != null) { + this.restTemplate.setRequestFactory(factory); + } + if (this.asyncRestTemplate != null) { + this.asyncRestTemplate.setAsyncRequestFactory(factory); } + return server; } - return sb.toString(); } /** * Mock ClientHttpRequestFactory that creates requests by iterating - * over the list of expected {@link RequestMatcherClientHttpRequest}'s. + * over the list of expected {@link DefaultRequestExpectation}'s. */ - private class RequestMatcherClientHttpRequestFactory - implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { - - private Iterator<RequestMatcherClientHttpRequest> requestIterator; + private class MockClientHttpRequestFactory implements ClientHttpRequestFactory, AsyncClientHttpRequestFactory { @Override - public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) throws IOException { + public ClientHttpRequest createRequest(URI uri, HttpMethod httpMethod) { return createRequestInternal(uri, httpMethod); } @Override - public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) throws IOException { + public AsyncClientHttpRequest createAsyncRequest(URI uri, HttpMethod httpMethod) { return createRequestInternal(uri, httpMethod); } - private RequestMatcherClientHttpRequest createRequestInternal(URI uri, HttpMethod httpMethod) { + private MockAsyncClientHttpRequest createRequestInternal(URI uri, HttpMethod method) { Assert.notNull(uri, "'uri' must not be null"); - Assert.notNull(httpMethod, "'httpMethod' must not be null"); - - if (this.requestIterator == null) { - this.requestIterator = MockRestServiceServer.this.expectedRequests.iterator(); - } - if (!this.requestIterator.hasNext()) { - throw new AssertionError("No further requests expected: HTTP " + httpMethod + " " + uri); - } + Assert.notNull(method, "'httpMethod' must not be null"); - RequestMatcherClientHttpRequest request = this.requestIterator.next(); - request.setURI(uri); - request.setMethod(httpMethod); + return new MockAsyncClientHttpRequest(method, uri) { - MockRestServiceServer.this.actualRequests.add(request); - return request; + @Override + protected ClientHttpResponse executeInternal() throws IOException { + ClientHttpResponse response = expectationManager.validateRequest(this); + setResponse(response); + return response; + } + }; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java new file mode 100644 index 00000000..b2d8f144 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java @@ -0,0 +1,42 @@ +/* + * 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.web.client; + +/** + * An extension of {@code ResponseActions} that also implements + * {@code RequestMatcher} and {@code ResponseCreator} + * + * <p>While {@code ResponseActions} is the API for defining expectations this + * sub-interface is the internal SPI for matching these expectations to actual + * requests and for creating responses. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface RequestExpectation extends ResponseActions, RequestMatcher, ResponseCreator { + + /** + * Whether there is a remaining count of invocations for this expectation. + */ + boolean hasRemainingCount(); + + /** + * Whether the requirements for this request expectation have been met. + */ + boolean isSatisfied(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java new file mode 100644 index 00000000..a79f9210 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java @@ -0,0 +1,63 @@ +/* + * 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.web.client; + +import java.io.IOException; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * Abstraction for creating HTTP request expectations, applying them to actual + * requests (in strict or random order), and verifying whether expectations + * have been met. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public interface RequestExpectationManager { + + /** + * Set up a new request expectation. The returned {@link ResponseActions} is + * used to add more expectations and define a response. + * @param requestMatcher a request expectation + * @return for setting up further expectations and define a response + */ + ResponseActions expectRequest(ExpectedCount count, RequestMatcher requestMatcher); + + /** + * Validate the given actual request against the declared expectations. + * Is successful return the mock response to use or raise an error. + * @param request the request + * @return the response to return if the request was validated. + * @throws AssertionError when some expectations were not met + * @throws IOException + */ + ClientHttpResponse validateRequest(ClientHttpRequest request) throws IOException; + + /** + * Verify that all expectations have been met. + * @throws AssertionError when some expectations were not met + */ + void verify(); + + /** + * Reset the internal state removing all expectations and recorded requests. + */ + void reset(); + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java index bc169f48..14237729 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java @@ -23,6 +23,9 @@ import org.springframework.http.client.ClientHttpRequest; /** * A contract for matching requests to expectations. * + * <p>See {@link org.springframework.test.web.client.match.MockRestRequestMatchers + * MockRestRequestMatchers} for static factory methods. + * * @author Craig Walls * @since 3.2 */ diff --git a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java b/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java deleted file mode 100644 index 1a01328b..00000000 --- a/spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java +++ /dev/null @@ -1,81 +0,0 @@ -/* - * 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.web.client; - -import java.io.IOException; -import java.util.LinkedList; -import java.util.List; - -import org.springframework.http.client.ClientHttpResponse; -import org.springframework.mock.http.client.MockAsyncClientHttpRequest; -import org.springframework.util.Assert; - -/** - * A specialization of {@code MockClientHttpRequest} that matches the request - * against a set of expectations, via {@link RequestMatcher} instances. The - * expectations are checked when the request is executed. This class also uses a - * {@link ResponseCreator} to create the response. - * - * @author Craig Walls - * @author Rossen Stoyanchev - * @since 3.2 - */ -class RequestMatcherClientHttpRequest extends MockAsyncClientHttpRequest implements ResponseActions { - - private final List<RequestMatcher> requestMatchers = new LinkedList<RequestMatcher>(); - - private ResponseCreator responseCreator; - - - public RequestMatcherClientHttpRequest(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "RequestMatcher is required"); - this.requestMatchers.add(requestMatcher); - } - - - @Override - public ResponseActions andExpect(RequestMatcher requestMatcher) { - Assert.notNull(requestMatcher, "RequestMatcher is required"); - this.requestMatchers.add(requestMatcher); - return this; - } - - @Override - public void andRespond(ResponseCreator responseCreator) { - Assert.notNull(responseCreator, "ResponseCreator is required"); - this.responseCreator = responseCreator; - } - - @Override - public ClientHttpResponse executeInternal() throws IOException { - if (this.requestMatchers.isEmpty()) { - throw new AssertionError("No request expectations to execute"); - } - - if (this.responseCreator == null) { - throw new AssertionError("No ResponseCreator was set up. Add it after request expectations, " + - "e.g. MockRestServiceServer.expect(requestTo(\"/foo\")).andRespond(withSuccess())"); - } - - for (RequestMatcher requestMatcher : this.requestMatchers) { - requestMatcher.match(this); - } - setResponse(this.responseCreator.createResponse(this)); - return super.executeInternal(); - } - -} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java new file mode 100644 index 00000000..dbcf3852 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java @@ -0,0 +1,82 @@ +/* + * 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.web.client; + +import java.io.IOException; +import java.util.Iterator; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.util.Assert; + +/** + * Simple {@code RequestExpectationManager} that matches requests to expectations + * sequentially, i.e. in the order of declaration of expectations. + * + * <p>When request expectations have an expected count greater than one, + * only the first execution is expected to match the order of declaration. + * Subsequent request executions may be inserted anywhere thereafter. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class SimpleRequestExpectationManager extends AbstractRequestExpectationManager { + + private Iterator<RequestExpectation> expectationIterator; + + private final RequestExpectationGroup repeatExpectations = new RequestExpectationGroup(); + + + @Override + protected void afterExpectationsDeclared() { + Assert.state(this.expectationIterator == null); + this.expectationIterator = getExpectations().iterator(); + } + + @Override + public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { + RequestExpectation expectation; + try { + expectation = next(request); + expectation.match(request); + } + catch (AssertionError error) { + expectation = this.repeatExpectations.findExpectation(request); + if (expectation == null) { + throw error; + } + } + ClientHttpResponse response = expectation.createResponse(request); + this.repeatExpectations.update(expectation); + return response; + } + + private RequestExpectation next(ClientHttpRequest request) { + if (this.expectationIterator.hasNext()) { + return this.expectationIterator.next(); + } + throw createUnexpectedRequestError(request); + } + + @Override + public void reset() { + super.reset(); + this.expectationIterator = null; + this.repeatExpectations.reset(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java new file mode 100644 index 00000000..42c1a6aa --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java @@ -0,0 +1,58 @@ +/* + * 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.web.client; + +import java.io.IOException; + +import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.client.ClientHttpResponse; + +/** + * {@code RequestExpectationManager} that matches requests to expectations + * regardless of the order of declaration of expected requests. + * + * @author Rossen Stoyanchev + * @since 4.3 + */ +public class UnorderedRequestExpectationManager extends AbstractRequestExpectationManager { + + private final RequestExpectationGroup remainingExpectations = new RequestExpectationGroup(); + + + @Override + protected void afterExpectationsDeclared() { + this.remainingExpectations.updateAll(getExpectations()); + } + + @Override + public ClientHttpResponse validateRequestInternal(ClientHttpRequest request) throws IOException { + RequestExpectation expectation = this.remainingExpectations.findExpectation(request); + if (expectation != null) { + ClientHttpResponse response = expectation.createResponse(request); + this.remainingExpectations.update(expectation); + return response; + } + throw createUnexpectedRequestError(request); + } + + @Override + public void reset() { + super.reset(); + this.remainingExpectations.reset(); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java index 4acbf82e..e32a68aa 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,18 +16,24 @@ package org.springframework.test.web.client.match; +import java.io.ByteArrayInputStream; import java.io.IOException; +import java.io.InputStream; import javax.xml.transform.Source; import javax.xml.transform.dom.DOMSource; import org.hamcrest.Matcher; import org.w3c.dom.Node; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; import org.springframework.http.MediaType; import org.springframework.http.client.ClientHttpRequest; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.mock.http.client.MockClientHttpRequest; import org.springframework.test.util.XmlExpectationsHelper; import org.springframework.test.web.client.RequestMatcher; +import org.springframework.util.MultiValueMap; import static org.hamcrest.MatcherAssert.*; import static org.springframework.test.util.AssertionErrors.*; @@ -137,13 +143,36 @@ public class ContentRequestMatchers { } /** + * Parse the body as form data and compare to the given {@code MultiValueMap}. + * @since 4.3 + */ + public RequestMatcher formData(final MultiValueMap<String, String> expectedContent) { + return new RequestMatcher() { + @Override + public void match(final ClientHttpRequest request) throws IOException, AssertionError { + HttpInputMessage inputMessage = new HttpInputMessage() { + @Override + public InputStream getBody() throws IOException { + MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; + return new ByteArrayInputStream(mockRequest.getBodyAsBytes()); + } + @Override + public HttpHeaders getHeaders() { + return request.getHeaders(); + } + }; + FormHttpMessageConverter converter = new FormHttpMessageConverter(); + assertEquals("Request content", expectedContent, converter.read(null, inputMessage)); + } + }; + } + + /** * Parse the request body and the given String as XML and assert that the * two are "similar" - i.e. they contain the same elements and attributes * regardless of order. - * * <p>Use of this matcher assumes the * <a href="http://xmlunit.sourceforge.net/">XMLUnit<a/> library is available. - * * @param expectedXmlContent the expected XML content */ public RequestMatcher xml(final String expectedXmlContent) { @@ -180,6 +209,7 @@ public class ContentRequestMatchers { }; } + /** * Abstract base class for XML {@link RequestMatcher}'s. */ @@ -191,12 +221,13 @@ public class ContentRequestMatchers { MockClientHttpRequest mockRequest = (MockClientHttpRequest) request; matchInternal(mockRequest); } - catch (Exception e) { - throw new AssertionError("Failed to parse expected or actual XML request content: " + e.getMessage()); + catch (Exception ex) { + throw new AssertionError("Failed to parse expected or actual XML request content: " + ex.getMessage()); } } protected abstract void matchInternal(MockClientHttpRequest request) throws Exception; - } + } + diff --git a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java index 318e9d88..d9d78bb9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java @@ -123,7 +123,7 @@ public abstract class MockRestRequestMatchers { /** * Assert request header values with the given Hamcrest matcher. */ - @SuppressWarnings("unchecked") + @SafeVarargs public static RequestMatcher header(final String name, final Matcher<? super String>... matchers) { return new RequestMatcher() { @Override diff --git a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java index 3f2be1ae..213bc5d7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java +++ b/spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -13,12 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + package org.springframework.test.web.client.response; import java.io.IOException; import java.io.InputStream; -import java.io.UnsupportedEncodingException; import java.net.URI; +import java.nio.charset.Charset; import org.springframework.core.io.Resource; import org.springframework.http.HttpHeaders; @@ -38,6 +39,9 @@ import org.springframework.util.Assert; */ public class DefaultResponseCreator implements ResponseCreator { + private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + + private byte[] content; private Resource contentResource; @@ -56,6 +60,7 @@ public class DefaultResponseCreator implements ResponseCreator { this.statusCode = statusCode; } + @Override public ClientHttpResponse createResponse(ClientHttpRequest request) throws IOException { MockClientHttpResponse response; @@ -74,13 +79,7 @@ public class DefaultResponseCreator implements ResponseCreator { * Set the body as a UTF-8 String. */ public DefaultResponseCreator body(String content) { - try { - this.content = content.getBytes("UTF-8"); - } - catch (UnsupportedEncodingException e) { - // should not happen, UTF-8 is always supported - throw new IllegalStateException(e); - } + this.content = content.getBytes(UTF8_CHARSET); return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java index 3db5711d..1f64c74e 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java @@ -37,8 +37,7 @@ import org.springframework.util.Assert; * WebClient webClient = new WebClient(); * * MockMvc mockMvc = ... - * MockMvcWebConnection mockConnection = new MockMvcWebConnection(mockMvc); - * mockConnection.setWebClient(webClient); + * MockMvcWebConnection mockConnection = new MockMvcWebConnection(mockMvc, webClient); * * WebRequestMatcher cdnMatcher = new UrlRegexRequestMatcher(".*?//code.jquery.com/.*"); * WebConnection httpConnection = new HttpWebConnection(webClient); diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java index 497bf4ab..597ac8f4 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java @@ -106,7 +106,7 @@ public class MockMvcWebClientBuilder extends MockMvcWebConnectionBuilderSupport< */ public MockMvcWebClientBuilder withDelegate(WebClient webClient) { Assert.notNull(webClient, "WebClient must not be null"); - webClient.setWebConnection(createConnection(webClient.getWebConnection())); + webClient.setWebConnection(createConnection(webClient)); this.webClient = webClient; return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java index 2f19cea7..fd4ef702 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -17,13 +17,16 @@ package org.springframework.test.web.servlet.htmlunit; import java.io.IOException; +import java.util.Date; import java.util.HashMap; import java.util.Map; +import com.gargoylesoftware.htmlunit.CookieManager; import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebConnection; import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebResponse; +import com.gargoylesoftware.htmlunit.util.Cookie; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @@ -41,8 +44,7 @@ import org.springframework.util.Assert; * <pre class="code"> * WebClient webClient = new WebClient(); * MockMvc mockMvc = ... - * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc); - * mockConnection.setWebClient(webClient); + * MockMvcWebConnection webConnection = new MockMvcWebConnection(mockMvc, webClient); * webClient.setWebConnection(webConnection); * * // Use webClient as normal ... @@ -70,9 +72,10 @@ public final class MockMvcWebConnection implements WebConnection { * <p>For example, the URL {@code http://localhost/test/this} would use * {@code ""} as the context path. * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @param webClient the {@link WebClient} to use. never {@code null} */ - public MockMvcWebConnection(MockMvc mockMvc) { - this(mockMvc, ""); + public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient) { + this(mockMvc, webClient, ""); } /** @@ -83,17 +86,68 @@ public final class MockMvcWebConnection implements WebConnection { * which states that it can be an empty string and otherwise must start * with a "/" character and not end with a "/" character. * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @param webClient the {@link WebClient} to use. never {@code null} * @param contextPath the contextPath to use */ - public MockMvcWebConnection(MockMvc mockMvc, String contextPath) { + public MockMvcWebConnection(MockMvc mockMvc, WebClient webClient, String contextPath) { Assert.notNull(mockMvc, "MockMvc must not be null"); + Assert.notNull(webClient, "WebClient must not be null"); validateContextPath(contextPath); - this.webClient = new WebClient(); + this.webClient = webClient; this.mockMvc = mockMvc; this.contextPath = contextPath; } + /** + * Create a new instance that assumes the context path of the application + * is {@code ""} (i.e., the root context). + * <p>For example, the URL {@code http://localhost/test/this} would use + * {@code ""} as the context path. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @deprecated Use {@link #MockMvcWebConnection(MockMvc, WebClient)} + */ + @Deprecated + public MockMvcWebConnection(MockMvc mockMvc) { + this(mockMvc, ""); + } + + /** + * Create a new instance with the specified context path. + * <p>The path may be {@code null} in which case the first path segment + * of the URL is turned into the contextPath. Otherwise it must conform + * to {@link javax.servlet.http.HttpServletRequest#getContextPath()} + * which states that it can be an empty string and otherwise must start + * with a "/" character and not end with a "/" character. + * @param mockMvc the {@code MockMvc} instance to use; never {@code null} + * @param contextPath the contextPath to use + * @deprecated use {@link #MockMvcWebConnection(MockMvc, WebClient, String)} + */ + @Deprecated + public MockMvcWebConnection(MockMvc mockMvc, String contextPath) { + this(mockMvc, new WebClient(), contextPath); + } + + /** + * Validate the supplied {@code contextPath}. + * <p>If the value is not {@code null}, it must conform to + * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which + * states that it can be an empty string and otherwise must start with + * a "/" character and not end with a "/" character. + * @param contextPath the path to validate + */ + static void validateContextPath(String contextPath) { + if (contextPath == null || "".equals(contextPath)) { + return; + } + if (!contextPath.startsWith("/")) { + throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'."); + } + if (contextPath.endsWith("/")) { + throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'."); + } + } + public void setWebClient(WebClient webClient) { Assert.notNull(webClient, "WebClient must not be null"); @@ -113,6 +167,7 @@ public final class MockMvcWebConnection implements WebConnection { httpServletResponse = getResponse(requestBuilder); forwardedUrl = httpServletResponse.getForwardedUrl(); } + storeCookies(webRequest, httpServletResponse.getCookies()); return new MockWebResponseBuilder(startTime, webRequest, httpServletResponse).build(); } @@ -129,29 +184,29 @@ public final class MockMvcWebConnection implements WebConnection { return resultActions.andReturn().getResponse(); } - @Override - public void close() { - } - - - /** - * Validate the supplied {@code contextPath}. - * <p>If the value is not {@code null}, it must conform to - * {@link javax.servlet.http.HttpServletRequest#getContextPath()} which - * states that it can be an empty string and otherwise must start with - * a "/" character and not end with a "/" character. - * @param contextPath the path to validate - */ - static void validateContextPath(String contextPath) { - if (contextPath == null || "".equals(contextPath)) { + private void storeCookies(WebRequest webRequest, javax.servlet.http.Cookie[] cookies) { + if (cookies == null) { return; } - if (!contextPath.startsWith("/")) { - throw new IllegalArgumentException("contextPath '" + contextPath + "' must start with '/'."); - } - if (contextPath.endsWith("/")) { - throw new IllegalArgumentException("contextPath '" + contextPath + "' must not end with '/'."); + Date now = new Date(); + CookieManager cookieManager = this.webClient.getCookieManager(); + for (javax.servlet.http.Cookie cookie : cookies) { + if (cookie.getDomain() == null) { + cookie.setDomain(webRequest.getUrl().getHost()); + } + Cookie toManage = MockWebResponseBuilder.createCookie(cookie); + Date expires = toManage.getExpires(); + if (expires == null || expires.after(now)) { + cookieManager.addCookie(toManage); + } + else { + cookieManager.removeCookie(toManage); + } } } + @Override + public void close() { + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java index cc957dca..c173fdfb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -19,9 +19,11 @@ package org.springframework.test.web.servlet.htmlunit; import java.util.ArrayList; import java.util.List; +import com.gargoylesoftware.htmlunit.WebClient; import com.gargoylesoftware.htmlunit.WebConnection; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.htmlunit.DelegatingWebConnection.DelegateWebConnection; import org.springframework.test.web.servlet.setup.MockMvcBuilders; import org.springframework.test.web.servlet.setup.MockMvcConfigurer; import org.springframework.util.Assert; @@ -43,7 +45,7 @@ public abstract class MockMvcWebConnectionBuilderSupport<T extends MockMvcWebCon private final MockMvc mockMvc; - private final List<WebRequestMatcher> mockMvcRequestMatchers = new ArrayList<WebRequestMatcher>(); + private final List<WebRequestMatcher> requestMatchers = new ArrayList<WebRequestMatcher>(); private String contextPath = ""; @@ -57,7 +59,7 @@ public abstract class MockMvcWebConnectionBuilderSupport<T extends MockMvcWebCon protected MockMvcWebConnectionBuilderSupport(MockMvc mockMvc) { Assert.notNull(mockMvc, "MockMvc must not be null"); this.mockMvc = mockMvc; - this.mockMvcRequestMatchers.add(new HostRequestMatcher("localhost")); + this.requestMatchers.add(new HostRequestMatcher("localhost")); } /** @@ -116,7 +118,7 @@ public abstract class MockMvcWebConnectionBuilderSupport<T extends MockMvcWebCon @SuppressWarnings("unchecked") public T useMockMvc(WebRequestMatcher... matchers) { for (WebRequestMatcher matcher : matchers) { - this.mockMvcRequestMatchers.add(matcher); + this.requestMatchers.add(matcher); } return (T) this; } @@ -130,7 +132,7 @@ public abstract class MockMvcWebConnectionBuilderSupport<T extends MockMvcWebCon */ @SuppressWarnings("unchecked") public T useMockMvcForHosts(String... hosts) { - this.mockMvcRequestMatchers.add(new HostRequestMatcher(hosts)); + this.requestMatchers.add(new HostRequestMatcher(hosts)); return (T) this; } @@ -145,21 +147,41 @@ public abstract class MockMvcWebConnectionBuilderSupport<T extends MockMvcWebCon * @see #alwaysUseMockMvc() * @see #useMockMvc(WebRequestMatcher...) * @see #useMockMvcForHosts(String...) + * @deprecated Use {@link #createConnection(WebClient)} instead */ + @Deprecated protected final WebConnection createConnection(WebConnection defaultConnection) { Assert.notNull(defaultConnection, "Default WebConnection must not be null"); - MockMvcWebConnection mockMvcWebConnection = new MockMvcWebConnection(this.mockMvc, this.contextPath); + return createConnection(new WebClient(), defaultConnection); + } + /** + * Create a new {@link WebConnection} that will use a {@link MockMvc} + * instance if one of the specified {@link WebRequestMatcher} instances + * matches. + * @param webClient the WebClient to use if none of the specified + * {@code WebRequestMatcher} instances matches (never {@code null}) + * @return a new {@code WebConnection} that will use a {@code MockMvc} + * instance if one of the specified {@code WebRequestMatcher} matches + * @see #alwaysUseMockMvc() + * @see #useMockMvc(WebRequestMatcher...) + * @see #useMockMvcForHosts(String...) + * @since 4.3 + */ + protected final WebConnection createConnection(WebClient webClient) { + Assert.notNull(webClient, "WebClient must not be null"); + return createConnection(webClient, webClient.getWebConnection()); + } + + private WebConnection createConnection(WebClient webClient, WebConnection defaultConnection) { + WebConnection connection = new MockMvcWebConnection(this.mockMvc, webClient, this.contextPath); if (this.alwaysUseMockMvc) { - return mockMvcWebConnection; + return connection; } - - List<DelegatingWebConnection.DelegateWebConnection> delegates = new ArrayList<DelegatingWebConnection.DelegateWebConnection>( - this.mockMvcRequestMatchers.size()); - for (WebRequestMatcher matcher : this.mockMvcRequestMatchers) { - delegates.add(new DelegatingWebConnection.DelegateWebConnection(matcher, mockMvcWebConnection)); + List<DelegateWebConnection> delegates = new ArrayList<DelegateWebConnection>(this.requestMatchers.size()); + for (WebRequestMatcher matcher : this.requestMatchers) { + delegates.add(new DelegateWebConnection(matcher, connection)); } - return new DelegatingWebConnection(defaultConnection, delegates); } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java index 7ead8e13..fd716f47 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -19,13 +19,17 @@ package org.springframework.test.web.servlet.htmlunit; import java.io.IOException; import java.util.ArrayList; import java.util.Collection; +import java.util.Date; import java.util.List; +import javax.servlet.http.Cookie; + import com.gargoylesoftware.htmlunit.WebRequest; import com.gargoylesoftware.htmlunit.WebResponse; import com.gargoylesoftware.htmlunit.WebResponseData; import com.gargoylesoftware.htmlunit.util.NameValuePair; +import org.apache.http.impl.cookie.BasicClientCookie; import org.springframework.http.HttpStatus; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.util.Assert; @@ -34,6 +38,7 @@ import org.springframework.util.StringUtils; /** * @author Rob Winch * @author Sam Brannen + * @author Rossen Stoyanchev * @since 4.2 */ final class MockWebResponseBuilder { @@ -100,7 +105,30 @@ final class MockWebResponseBuilder { if (location != null) { responseHeaders.add(new NameValuePair("Location", location)); } + for (Cookie cookie : this.response.getCookies()) { + responseHeaders.add(new NameValuePair("Set-Cookie", valueOfCookie(cookie))); + } return responseHeaders; } + private String valueOfCookie(Cookie cookie) { + return createCookie(cookie).toString(); + } + + static com.gargoylesoftware.htmlunit.util.Cookie createCookie(Cookie cookie) { + Date expires = null; + if (cookie.getMaxAge() > -1) { + expires = new Date(System.currentTimeMillis() + cookie.getMaxAge() * 1000); + } + BasicClientCookie result = new BasicClientCookie(cookie.getName(), cookie.getValue()); + result.setDomain(cookie.getDomain()); + result.setComment(cookie.getComment()); + result.setExpiryDate(expires); + result.setPath(cookie.getPath()); + result.setSecure(cookie.getSecure()); + if(cookie.isHttpOnly()) { + result.setAttribute("httponly", "true"); + } + return new com.gargoylesoftware.htmlunit.util.Cookie(result); + } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java index 9454c0a3..9a5ab8ac 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java @@ -129,7 +129,7 @@ public class MockMvcHtmlUnitDriverBuilder extends MockMvcWebConnectionBuilderSup public MockMvcHtmlUnitDriverBuilder withDelegate(WebConnectionHtmlUnitDriver driver) { Assert.notNull(driver, "HtmlUnitDriver must not be null"); driver.setJavascriptEnabled(this.javascriptEnabled); - driver.setWebConnection(createConnection(driver.getWebConnection())); + driver.setWebConnection(createConnection(driver.getWebClient())); this.driver = driver; return this; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java index bdb67b87..e8fc5f18 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java @@ -88,6 +88,14 @@ public class WebConnectionHtmlUnitDriver extends HtmlUnitDriver { } /** + * Return the current {@link WebClient}. + * @since 4.3 + */ + public WebClient getWebClient() { + return this.webClient; + } + + /** * Set the {@link WebConnection} to be used with the {@link WebClient}. * @param webConnection the {@code WebConnection} to use (never {@code null}) */ diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java index 1663bd2f..d181a46f 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,8 +16,12 @@ package org.springframework.test.web.servlet.request; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; import java.io.UnsupportedEncodingException; import java.net.URI; +import java.nio.charset.Charset; import java.security.Principal; import java.util.ArrayList; import java.util.Arrays; @@ -33,8 +37,10 @@ import javax.servlet.http.Cookie; import org.springframework.beans.Mergeable; import org.springframework.beans.factory.NoSuchBeanDefinitionException; import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpInputMessage; import org.springframework.http.HttpMethod; import org.springframework.http.MediaType; +import org.springframework.http.converter.FormHttpMessageConverter; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.mock.web.MockHttpSession; @@ -60,16 +66,23 @@ import org.springframework.web.util.UriUtils; * * <p>Application tests will typically access this builder through the static factory * methods in {@link MockMvcRequestBuilders}. + * <p>Although this class cannot be extended, additional ways to initialize + * the {@code MockHttpServletRequest} can be plugged in via + * {@link #with(RequestPostProcessor)}. * * @author Rossen Stoyanchev * @author Arjen Poutsma * @author Sam Brannen + * @author Kamill Sokol * @since 3.2 */ public class MockHttpServletRequestBuilder implements ConfigurableSmartRequestBuilder<MockHttpServletRequestBuilder>, Mergeable { - private final HttpMethod method; + private static final Charset UTF8_CHARSET = Charset.forName("UTF-8"); + + + private final String method; private final URI url; @@ -119,27 +132,33 @@ public class MockHttpServletRequestBuilder * @param vars zero or more URL variables */ MockHttpServletRequestBuilder(HttpMethod httpMethod, String url, Object... vars) { - this(httpMethod, UriComponentsBuilder.fromUriString(url).buildAndExpand(vars).encode().toUri()); + this(httpMethod.name(), UriComponentsBuilder.fromUriString(url).buildAndExpand(vars).encode().toUri()); } /** - * Package private constructor. To get an instance, use static factory - * methods in {@link MockMvcRequestBuilders}. - * <p>Although this class cannot be extended, additional ways to initialize - * the {@code MockHttpServletRequest} can be plugged in via - * {@link #with(RequestPostProcessor)}. + * Alternative to {@link #MockHttpServletRequestBuilder(HttpMethod, String, Object...)} + * with a pre-built URI. * @param httpMethod the HTTP method (GET, POST, etc) * @param url the URL * @since 4.0.3 */ MockHttpServletRequestBuilder(HttpMethod httpMethod, URI url) { - Assert.notNull(httpMethod, "httpMethod is required"); - Assert.notNull(url, "url is required"); + this(httpMethod.name(), url); + } + + /** + * Alternative constructor for custom HTTP methods. + * @param httpMethod the HTTP method (GET, POST, etc) + * @param url the URL + * @since 4.3 + */ + MockHttpServletRequestBuilder(String httpMethod, URI url) { + Assert.notNull(httpMethod, "'httpMethod' is required"); + Assert.notNull(url, "'url' is required"); this.method = httpMethod; this.url = url; } - /** * Add a request parameter to the {@link MockHttpServletRequest}. * <p>If called more than once, new values get added to existing ones. @@ -257,12 +276,7 @@ public class MockHttpServletRequestBuilder * @param content the body content */ public MockHttpServletRequestBuilder content(String content) { - try { - this.content = content.getBytes("UTF-8"); - } - catch (UnsupportedEncodingException e) { - // should never happen - } + this.content = content.getBytes(UTF8_CHARSET); return this; } @@ -320,7 +334,7 @@ public class MockHttpServletRequestBuilder * @param sessionAttributes the session attributes */ public MockHttpServletRequestBuilder sessionAttrs(Map<String, Object> sessionAttributes) { - Assert.notEmpty(sessionAttributes, "'sessionAttrs' must not be empty"); + Assert.notEmpty(sessionAttributes, "'sessionAttributes' must not be empty"); for (String name : sessionAttributes.keySet()) { sessionAttr(name, sessionAttributes.get(name)); } @@ -342,7 +356,7 @@ public class MockHttpServletRequestBuilder * @param flashAttributes the flash attributes */ public MockHttpServletRequestBuilder flashAttrs(Map<String, Object> flashAttributes) { - Assert.notEmpty(flashAttributes, "'flashAttrs' must not be empty"); + Assert.notEmpty(flashAttributes, "'flashAttributes' must not be empty"); for (String name : flashAttributes.keySet()) { flashAttr(name, flashAttributes.get(name)); } @@ -585,7 +599,7 @@ public class MockHttpServletRequestBuilder request.setServerPort(this.url.getPort()); } - request.setMethod(this.method.name()); + request.setMethod(this.method); for (String name : this.headers.keySet()) { for (Object value : this.headers.get(name)) { @@ -593,24 +607,10 @@ public class MockHttpServletRequestBuilder } } - try { - if (this.url.getRawQuery() != null) { - request.setQueryString(this.url.getRawQuery()); - } - - MultiValueMap<String, String> queryParams = - UriComponentsBuilder.fromUri(this.url).build().getQueryParams(); - - for (Entry<String, List<String>> entry : queryParams.entrySet()) { - for (String value : entry.getValue()) { - value = (value != null) ? UriUtils.decode(value, "UTF-8") : null; - request.addParameter(UriUtils.decode(entry.getKey(), "UTF-8"), value); - } - } - } - catch (UnsupportedEncodingException ex) { - // shouldn't happen + if (this.url.getRawQuery() != null) { + request.setQueryString(this.url.getRawQuery()); } + addRequestParams(request, UriComponentsBuilder.fromUri(this.url).build().getQueryParams()); for (String name : this.parameters.keySet()) { for (String value : this.parameters.get(name)) { @@ -622,6 +622,13 @@ public class MockHttpServletRequestBuilder request.setContent(this.content); request.setCharacterEncoding(this.characterEncoding); + if (this.content != null && this.contentType != null) { + MediaType mediaType = MediaType.parseMediaType(this.contentType); + if (MediaType.APPLICATION_FORM_URLENCODED.includes(mediaType)) { + addRequestParams(request, parseFormData(mediaType)); + } + } + if (!ObjectUtils.isEmpty(this.cookies)) { request.setCookies(this.cookies.toArray(new Cookie[this.cookies.size()])); } @@ -685,6 +692,44 @@ public class MockHttpServletRequestBuilder request.setPathInfo(this.pathInfo); } + private void addRequestParams(MockHttpServletRequest request, MultiValueMap<String, String> map) { + try { + for (Entry<String, List<String>> entry : map.entrySet()) { + for (String value : entry.getValue()) { + value = (value != null) ? UriUtils.decode(value, "UTF-8") : null; + request.addParameter(UriUtils.decode(entry.getKey(), "UTF-8"), value); + } + } + } + catch (UnsupportedEncodingException ex) { + // shouldn't happen + } + } + + private MultiValueMap<String, String> parseFormData(final MediaType mediaType) { + MultiValueMap<String, String> map; + HttpInputMessage message = new HttpInputMessage() { + @Override + public InputStream getBody() throws IOException { + return new ByteArrayInputStream(content); + } + + @Override + public HttpHeaders getHeaders() { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(mediaType); + return headers; + } + }; + try { + map = new FormHttpMessageConverter().read(null, message); + } + catch (IOException ex) { + throw new IllegalStateException("Failed to parse form data in request body", ex); + } + return map; + } + private FlashMapManager getFlashMapManager(MockHttpServletRequest request) { FlashMapManager flashMapManager = null; try { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java index 571699e6..f0e21adb 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -42,6 +42,7 @@ import org.springframework.test.web.servlet.RequestBuilder; * @author Greg Turnquist * @author Sebastien Deleuze * @author Sam Brannen + * @author Kamill Sokol * @since 3.2 */ public abstract class MockMvcRequestBuilders { @@ -49,10 +50,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a GET request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.GET, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder get(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.GET, urlTemplate, urlVars); } /** @@ -67,10 +68,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a POST request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.POST, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder post(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.POST, urlTemplate, urlVars); } /** @@ -85,10 +86,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a PUT request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.PUT, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder put(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.PUT, urlTemplate, urlVars); } /** @@ -103,10 +104,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a PATCH request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder patch(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.PATCH, urlTemplate, urlVars); } /** @@ -121,10 +122,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a DELETE request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder delete(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.DELETE, urlTemplate, urlVars); } /** @@ -139,10 +140,10 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for an OPTIONS request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder options(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.OPTIONS, urlTemplate, urlVars); } /** @@ -157,11 +158,11 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a HEAD request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables * @since 4.1 */ - public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder head(String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(HttpMethod.HEAD, urlTemplate, urlVars); } /** @@ -175,12 +176,12 @@ public abstract class MockMvcRequestBuilders { /** * Create a {@link MockHttpServletRequestBuilder} for a request with the given HTTP method. - * @param httpMethod the HTTP method + * @param method the HTTP method (GET, POST, etc) * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockHttpServletRequestBuilder request(HttpMethod httpMethod, String urlTemplate, Object... urlVariables) { - return new MockHttpServletRequestBuilder(httpMethod, urlTemplate, urlVariables); + public static MockHttpServletRequestBuilder request(HttpMethod method, String urlTemplate, Object... urlVars) { + return new MockHttpServletRequestBuilder(method, urlTemplate, urlVars); } /** @@ -194,12 +195,22 @@ public abstract class MockMvcRequestBuilders { } /** + * Alternative factory method that allows for custom HTTP verbs (e.g. WebDAV). + * @param httpMethod the HTTP method + * @param uri the URL + * @since 4.3 + */ + public static MockHttpServletRequestBuilder request(String httpMethod, URI uri) { + return new MockHttpServletRequestBuilder(httpMethod, uri); + } + + /** * Create a {@link MockMultipartHttpServletRequestBuilder} for a multipart request. * @param urlTemplate a URL template; the resulting URL will be encoded - * @param urlVariables zero or more URL variables + * @param urlVars zero or more URL variables */ - public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVariables) { - return new MockMultipartHttpServletRequestBuilder(urlTemplate, urlVariables); + public static MockMultipartHttpServletRequestBuilder fileUpload(String urlTemplate, Object... urlVars) { + return new MockMultipartHttpServletRequestBuilder(urlTemplate, urlVars); } /** diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/HandlerResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/HandlerResultMatchers.java index 982eacdf..211ca9a7 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/HandlerResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/HandlerResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -24,22 +24,33 @@ import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; import org.springframework.util.ClassUtils; import org.springframework.web.method.HandlerMethod; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder; +import org.springframework.web.servlet.mvc.method.annotation.MvcUriComponentsBuilder.MethodInvocationInfo; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter; import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping; -import static org.hamcrest.MatcherAssert.*; -import static org.springframework.test.util.AssertionErrors.*; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertTrue; +import static org.springframework.test.util.AssertionErrors.fail; /** - * Factory for assertions on the selected handler. + * Factory for assertions on the selected handler or handler method. * <p>An instance of this class is typically accessed via * {@link MockMvcResultMatchers#handler}. * + * <p><strong>Note:</strong> Expectations that assert the controller method + * used to process the request work only for requests processed with + * {@link RequestMappingHandlerMapping} and {@link RequestMappingHandlerAdapter} + * which is used by default with the Spring MVC Java config and XML namespace. + * * @author Rossen Stoyanchev + * @author Sam Brannen * @since 3.2 */ public class HandlerResultMatchers { + /** * Protected constructor. * Use {@link MockMvcResultMatchers#handler()}. @@ -67,56 +78,92 @@ public class HandlerResultMatchers { } /** - * Assert the name of the controller method that processed the request with - * the given Hamcrest {@link Matcher}. - * <p>Use of this method implies annotated controllers are processed with - * {@link RequestMappingHandlerMapping} and {@link RequestMappingHandlerAdapter}. + * Assert the controller method used to process the request. + * <p>The expected method is specified through a "mock" controller method + * invocation similar to {@link MvcUriComponentsBuilder#fromMethodCall(Object)}. + * <p>For example, given this controller: + * <pre class="code"> + * @RestController + * public class SimpleController { + * + * @RequestMapping("/") + * public ResponseEntity<Void> handle() { + * return ResponseEntity.ok().build(); + * } + * } + * </pre> + * <p>A test that has statically imported {@link MvcUriComponentsBuilder#on} + * can be performed as follows: + * <pre class="code"> + * mockMvc.perform(get("/")) + * .andExpect(handler().methodCall(on(SimpleController.class).handle())); + * </pre> + * + * @param obj either the value returned from a "mock" controller invocation + * or the "mock" controller itself after an invocation + */ + public ResultMatcher methodCall(final Object obj) { + return new ResultMatcher() { + @Override + public void match(MvcResult result) throws Exception { + if (!MethodInvocationInfo.class.isInstance(obj)) { + fail(String.format("The supplied object [%s] is not an instance of %s. " + + "Ensure that you invoke the handler method via MvcUriComponentsBuilder.on().", + obj, MethodInvocationInfo.class.getName())); + } + MethodInvocationInfo invocationInfo = (MethodInvocationInfo) obj; + Method expected = invocationInfo.getControllerMethod(); + Method actual = getHandlerMethod(result).getMethod(); + assertEquals("Handler method", expected, actual); + } + }; + } + + /** + * Assert the name of the controller method used to process the request + * using the given Hamcrest {@link Matcher}. */ public ResultMatcher methodName(final Matcher<? super String> matcher) { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - Object handler = assertHandlerMethod(result); - assertThat("HandlerMethod", ((HandlerMethod) handler).getMethod().getName(), matcher); + HandlerMethod handlerMethod = getHandlerMethod(result); + assertThat("Handler method", handlerMethod.getMethod().getName(), matcher); } }; } /** - * Assert the name of the controller method that processed the request. - * <p>Use of this method implies annotated controllers are processed with - * {@link RequestMappingHandlerMapping} and {@link RequestMappingHandlerAdapter}. + * Assert the name of the controller method used to process the request. */ public ResultMatcher methodName(final String name) { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - Object handler = assertHandlerMethod(result); - assertEquals("HandlerMethod", name, ((HandlerMethod) handler).getMethod().getName()); + HandlerMethod handlerMethod = getHandlerMethod(result); + assertEquals("Handler method", name, handlerMethod.getMethod().getName()); } }; } /** - * Assert the controller method that processed the request. - * <p>Use of this method implies annotated controllers are processed with - * {@link RequestMappingHandlerMapping} and {@link RequestMappingHandlerAdapter}. + * Assert the controller method used to process the request. */ public ResultMatcher method(final Method method) { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - Object handler = assertHandlerMethod(result); - assertEquals("HandlerMethod", method, ((HandlerMethod) handler).getMethod()); + HandlerMethod handlerMethod = getHandlerMethod(result); + assertEquals("Handler method", method, handlerMethod.getMethod()); } }; } - private static Object assertHandlerMethod(MvcResult result) { + private static HandlerMethod getHandlerMethod(MvcResult result) { Object handler = result.getHandler(); assertTrue("No handler: ", handler != null); assertTrue("Not a HandlerMethod: " + handler, HandlerMethod.class.isInstance(handler)); - return handler; + return (HandlerMethod) handler; } } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/HeaderResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/HeaderResultMatchers.java index c57387ee..aac0bab9 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/HeaderResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/HeaderResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. @@ -16,22 +16,26 @@ package org.springframework.test.web.servlet.result; +import java.text.SimpleDateFormat; +import java.util.Arrays; +import java.util.Date; +import java.util.List; +import java.util.Locale; +import java.util.TimeZone; + import org.hamcrest.Matcher; +import org.springframework.mock.web.MockHttpServletResponse; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; -import static org.hamcrest.MatcherAssert.*; -import static org.springframework.test.util.AssertionErrors.*; - -import java.text.SimpleDateFormat; -import java.util.Date; -import java.util.Locale; -import java.util.TimeZone; +import static org.hamcrest.MatcherAssert.assertThat; +import static org.springframework.test.util.AssertionErrors.assertEquals; +import static org.springframework.test.util.AssertionErrors.assertTrue; /** * Factory for response header assertions. - * <p>An instance of this class is usually accessed via + * <p>An instance of this class is available via * {@link MockMvcResultMatchers#header}. * * @author Rossen Stoyanchev @@ -41,16 +45,18 @@ import java.util.TimeZone; */ public class HeaderResultMatchers { + /** * Protected constructor. - * Use {@link MockMvcResultMatchers#header()}. + * See {@link MockMvcResultMatchers#header()}. */ protected HeaderResultMatchers() { } + /** - * Assert the primary value of the named response header with the given - * Hamcrest {@link Matcher}. + * Assert the primary value of the response header with the given Hamcrest + * String {@code Matcher}. */ public ResultMatcher string(final String name, final Matcher<? super String> matcher) { return new ResultMatcher() { @@ -62,7 +68,22 @@ public class HeaderResultMatchers { } /** - * Assert the primary value of the named response header as a {@link String}. + * Assert the values of the response header with the given Hamcrest + * Iterable {@link Matcher}. + * @since 4.3 + */ + public <T> ResultMatcher stringValues(final String name, final Matcher<Iterable<String>> matcher) { + return new ResultMatcher() { + @Override + public void match(MvcResult result) { + List<String> values = result.getResponse().getHeaders(name); + assertThat("Response header " + name, values, matcher); + } + }; + } + + /** + * Assert the primary value of the response header as a String value. */ public ResultMatcher string(final String name, final String value) { return new ResultMatcher() { @@ -74,6 +95,20 @@ public class HeaderResultMatchers { } /** + * Assert the values of the response header as String values. + * @since 4.3 + */ + public ResultMatcher stringValues(final String name, final String... values) { + return new ResultMatcher() { + @Override + public void match(MvcResult result) { + List<Object> actual = result.getResponse().getHeaderValues(name); + assertEquals("Response header " + name, Arrays.asList(values), actual); + } + }; + } + + /** * Assert that the named response header does not exist. * @since 4.0 */ @@ -81,23 +116,25 @@ public class HeaderResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) { - assertTrue("Response should not contain header " + name, !result.getResponse().containsHeader(name)); + assertTrue("Response should not contain header " + name, + !result.getResponse().containsHeader(name)); } }; } /** * Assert the primary value of the named response header as a {@code long}. - * <p>The {@link ResultMatcher} returned by this method throws an {@link AssertionError} - * if the response does not contain the specified header, or if the supplied - * {@code value} does not match the primary value. + * <p>The {@link ResultMatcher} returned by this method throws an + * {@link AssertionError} if the response does not contain the specified + * header, or if the supplied {@code value} does not match the primary value. */ public ResultMatcher longValue(final String name, final long value) { return new ResultMatcher() { @Override public void match(MvcResult result) { - assertTrue("Response does not contain header " + name, result.getResponse().containsHeader(name)); - assertEquals("Response header " + name, value, Long.parseLong(result.getResponse().getHeader(name))); + MockHttpServletResponse response = result.getResponse(); + assertTrue("Response does not contain header " + name, response.containsHeader(name)); + assertEquals("Response header " + name, value, Long.parseLong(response.getHeader(name))); } }; } @@ -105,10 +142,9 @@ public class HeaderResultMatchers { /** * Assert the primary value of the named response header as a date String, * using the preferred date format described in RFC 7231. - * <p>The {@link ResultMatcher} returned by this method throws an {@link AssertionError} - * if the response does not contain the specified header, or if the supplied - * {@code value} does not match the primary value. - * + * <p>The {@link ResultMatcher} returned by this method throws an + * {@link AssertionError} if the response does not contain the specified + * header, or if the supplied {@code value} does not match the primary value. * @see <a href="https://tools.ietf.org/html/rfc7231#section-7.1.1.1">Section 7.1.1.1 of RFC 7231</a> * @since 4.2 */ @@ -118,8 +154,10 @@ public class HeaderResultMatchers { public void match(MvcResult result) { SimpleDateFormat format = new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss zzz", Locale.US); format.setTimeZone(TimeZone.getTimeZone("GMT")); - assertTrue("Response does not contain header " + name, result.getResponse().containsHeader(name)); - assertEquals("Response header " + name, format.format(new Date(value)), result.getResponse().getHeader(name)); + String formatted = format.format(new Date(value)); + MockHttpServletResponse response = result.getResponse(); + assertTrue("Response does not contain header " + name, response.containsHeader(name)); + assertEquals("Response header " + name, formatted, response.getHeader(name)); } }; } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java index 39377075..f572a372 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java @@ -16,12 +16,17 @@ package org.springframework.test.web.servlet.result; +import java.io.UnsupportedEncodingException; + import com.jayway.jsonpath.JsonPath; import org.hamcrest.Matcher; +import org.hamcrest.MatcherAssert; +import org.hamcrest.core.StringStartsWith; import org.springframework.test.util.JsonPathExpectationsHelper; import org.springframework.test.web.servlet.MvcResult; import org.springframework.test.web.servlet.ResultMatcher; +import org.springframework.util.StringUtils; /** * Factory for assertions on the response content using @@ -33,12 +38,15 @@ import org.springframework.test.web.servlet.ResultMatcher; * @author Rossen Stoyanchev * @author Craig Andrews * @author Sam Brannen + * @author Brian Clozel * @since 3.2 */ public class JsonPathResultMatchers { private final JsonPathExpectationsHelper jsonPathHelper; + private String prefix; + /** * Protected constructor. @@ -52,6 +60,19 @@ public class JsonPathResultMatchers { this.jsonPathHelper = new JsonPathExpectationsHelper(expression, args); } + /** + * Configures the current {@code JsonPathResultMatchers} instance + * to verify that the JSON payload is prepended with the given prefix. + * <p>Use this method if the JSON payloads are prefixed to avoid + * Cross Site Script Inclusion (XSSI) attacks. + * @param prefix the string prefix prepended to the actual JSON payload + * @since 4.3 + */ + public JsonPathResultMatchers prefix(String prefix) { + this.prefix = prefix; + return this; + } + /** * Evaluate the JSON path expression against the response content and @@ -61,7 +82,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValue(content, matcher); } }; @@ -75,7 +96,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - jsonPathHelper.assertValue(result.getResponse().getContentAsString(), expectedValue); + jsonPathHelper.assertValue(getContent(result), expectedValue); } }; } @@ -91,7 +112,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.exists(content); } }; @@ -108,7 +129,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.doesNotExist(content); } }; @@ -128,7 +149,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsEmpty(content); } }; @@ -148,7 +169,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsNotEmpty(content); } }; @@ -163,7 +184,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsString(content); } }; @@ -178,7 +199,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsBoolean(content); } }; @@ -193,7 +214,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsNumber(content); } }; @@ -207,7 +228,7 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsArray(content); } }; @@ -222,10 +243,29 @@ public class JsonPathResultMatchers { return new ResultMatcher() { @Override public void match(MvcResult result) throws Exception { - String content = result.getResponse().getContentAsString(); + String content = getContent(result); jsonPathHelper.assertValueIsMap(content); } }; } + private String getContent(MvcResult result) throws UnsupportedEncodingException { + String content = result.getResponse().getContentAsString(); + if (StringUtils.hasLength(this.prefix)) { + try { + String reason = String.format("Expected a JSON payload prefixed with \"%s\" but found: %s", + this.prefix, StringUtils.quote(content.substring(0, this.prefix.length()))); + MatcherAssert.assertThat(reason, content, StringStartsWith.startsWith(this.prefix)); + return content.substring(this.prefix.length()); + } + catch (StringIndexOutOfBoundsException oobe) { + throw new AssertionError( + "JSON prefix \"" + this.prefix + "\" not found, exception: " + oobe.getMessage()); + } + } + else { + return content; + } + } + } diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java index 32ad49fd..a12baf94 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2015 the original author or authors. + * 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. diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java index 5efa9100..f2eba236 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java @@ -33,6 +33,7 @@ import static org.springframework.test.util.AssertionErrors.*; * @author Keesun Baik * @author Rossen Stoyanchev * @author Sebastien Deleuze + * @author Brian Clozel * @since 3.2 */ public class StatusResultMatchers { @@ -562,6 +563,14 @@ public class StatusResultMatchers { } /** + * Assert the response status code is {@code HttpStatus.UNAVAILABLE_FOR_LEGAL_REASONS} (451). + * @since 4.3 + */ + public ResultMatcher isUnavailableForLegalReasons() { + return matcher(HttpStatus.valueOf(451)); + } + + /** * Assert the response status code is {@code HttpStatus.INTERNAL_SERVER_ERROR} (500). */ public ResultMatcher isInternalServerError() { diff --git a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java index 21dd9ebb..50c4db84 100644 --- a/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java +++ b/spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2014 the original author or authors. + * 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. @@ -55,7 +55,7 @@ public abstract class AbstractMockMvcBuilder<B extends AbstractMockMvcBuilder<B> private final List<ResultHandler> globalResultHandlers = new ArrayList<ResultHandler>(); - private Boolean dispatchOptions = Boolean.FALSE; + private Boolean dispatchOptions = Boolean.TRUE; private final List<MockMvcConfigurer> configurers = new ArrayList<MockMvcConfigurer>(4); |