summaryrefslogtreecommitdiff
path: root/spring-test/src/main/java/org/springframework
diff options
context:
space:
mode:
Diffstat (limited to 'spring-test/src/main/java/org/springframework')
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockHttpServletRequest.java4
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockHttpServletResponse.java4
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/MockServletContext.java11
-rw-r--r--spring-test/src/main/java/org/springframework/mock/web/portlet/ServletWrappingPortletContext.java5
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/Commit.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ProfileValueSourceConfiguration.java10
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/ProfileValueUtils.java48
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/Rollback.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/annotation/TestAnnotationUtils.java5
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ActiveProfiles.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/BootstrapUtils.java71
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/BootstrapWith.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextConfiguration.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextConfigurationAttributes.java30
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextCustomizer.java49
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextCustomizerFactory.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/ContextHierarchy.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/MergedContextConfiguration.java64
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestContextBootstrapper.java15
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestContextManager.java76
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestExecutionListener.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestExecutionListeners.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/TestPropertySource.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/cache/ContextCache.java24
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/cache/ContextCacheUtils.java54
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/cache/DefaultContextCache.java93
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/jdbc/Sql.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/jdbc/SqlScriptsTestExecutionListener.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/AbstractJUnit4SpringContextTests.java10
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/AbstractTransactionalJUnit4SpringContextTests.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/SpringJUnit4ClassRunner.java27
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/SpringRunner.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/package-info.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringClassRule.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/rules/SpringMethodRule.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/ProfileValueChecker.java24
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestClassCallbacks.java8
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/junit4/statements/RunAfterTestMethodCallbacks.java8
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractContextLoader.java24
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractDelegatingSmartContextLoader.java17
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractGenericContextLoader.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AbstractTestContextBootstrapper.java132
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/AnnotationConfigContextLoaderUtils.java6
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/ContextLoaderUtils.java18
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/support/TestPropertySourceUtils.java127
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/testng/AbstractTransactionalTestNGSpringContextTests.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/AfterTransaction.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/BeforeTransaction.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TestContextTransactionUtils.java41
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TransactionConfiguration.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/transaction/TransactionalTestExecutionListener.java166
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/AbstractGenericWebContextLoader.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/ServletTestExecutionListener.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebAppConfiguration.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebMergedContextConfiguration.java44
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/WebTestContextBootstrapper.java22
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainer.java135
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizer.java52
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/socket/MockServerContainerContextCustomizerFactory.java73
-rw-r--r--spring-test/src/main/java/org/springframework/test/context/web/socket/package-info.java4
-rw-r--r--spring-test/src/main/java/org/springframework/test/jdbc/JdbcTestUtils.java7
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/AopTestUtils.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/AssertionErrors.java14
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/JsonPathExpectationsHelper.java39
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/MetaAnnotationUtils.java43
-rw-r--r--spring-test/src/main/java/org/springframework/test/util/ReflectionTestUtils.java71
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/AbstractRequestExpectationManager.java189
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/DefaultRequestExpectation.java146
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/ExpectedCount.java118
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/MockMvcClientHttpRequestFactory.java8
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/MockRestServiceServer.java308
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/RequestExpectation.java42
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/RequestExpectationManager.java63
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/RequestMatcher.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/RequestMatcherClientHttpRequest.java81
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/SimpleRequestExpectationManager.java82
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/UnorderedRequestExpectationManager.java58
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/match/ContentRequestMatchers.java43
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/match/MockRestRequestMatchers.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/client/response/DefaultResponseCreator.java17
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/DelegatingWebConnection.java3
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebClientBuilder.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnection.java109
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockMvcWebConnectionBuilderSupport.java48
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/MockWebResponseBuilder.java30
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/MockMvcHtmlUnitDriverBuilder.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/htmlunit/webdriver/WebConnectionHtmlUnitDriver.java8
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/request/MockHttpServletRequestBuilder.java119
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/request/MockMvcRequestBuilders.java69
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/result/HandlerResultMatchers.java91
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/result/HeaderResultMatchers.java88
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/result/JsonPathResultMatchers.java62
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/result/MockMvcResultMatchers.java2
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/result/StatusResultMatchers.java9
-rw-r--r--spring-test/src/main/java/org/springframework/test/web/servlet/setup/AbstractMockMvcBuilder.java4
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
* &#064;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
* &#064;ProfileValueSourceConfiguration} annotation and instantiates a new
* instance of that type.
- * <p>
- * If {@link ProfileValueSourceConfiguration
+ * <p>If {@link ProfileValueSourceConfiguration
* &#064;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
* &#064;IfProfileValue} annotation at the class level.
- * <p>
- * Defaults to {@code true} if no {@link IfProfileValue
+ * <p>Defaults to {@code true} if no {@link IfProfileValue
* &#064;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 {
* &#064;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
* &#064;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 {
* &#064;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
* &#064;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 &#064;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:&lt;application/json&gt; but was:&lt;text/plain&gt;
* </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);
* &#47;&#47; 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">
+ * &#064;RestController
+ * public class SimpleController {
+ *
+ * &#064;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);