Commit 1cd781b2 authored by Andy Wilkinson's avatar Andy Wilkinson

Make spring-boot-test compatible with Mockito 2.1 and 2.2

We use some internal Mockito classes and some  breaking API changes
have been made to them in Mockito 2. This commit introduces a utility
class, SpringBootMockUtil, to shield our code from these differences.
Mockito 1 is called directly and Mockito 2 is called via reflection.

To allow these changes to be tested, FilteredClassPathRunner has been
enhanced to also support overriding a dependency on the class path.
As a result it has been renamed to ModifiedClassPathRunner. The new
ClassPathOverrides annotation can be used to provide the Maven
coordinates of one or more dependencies that should be resolved and
added to the class path. Such additions are added to the start of
the class path so that they override any existing dependency that
contains the same classes.

Closes gh-6520
parent e0bf8ad2
......@@ -22,7 +22,7 @@ import org.junit.runner.RunWith;
import org.springframework.boot.test.util.EnvironmentTestUtils;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -33,7 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Stephane Nicoll
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-security-*.jar")
public class ManagementServerPropertiesAutoConfigurationNoSecurityTests {
......
......@@ -31,7 +31,7 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.autoconfigure.web.HttpMessageConvertersAutoConfiguration;
import org.springframework.boot.autoconfigure.web.WebMvcAutoConfiguration;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.mock.web.MockServletContext;
......@@ -49,7 +49,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-security-*.jar")
public class NoSpringSecurityHealthMvcEndpointIntegrationTests {
......
......@@ -22,7 +22,7 @@ import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.condition.OnBeanCondition.BeanTypeDeductionException;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
......@@ -38,7 +38,7 @@ import static org.junit.Assert.fail;
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("jackson-core-*.jar")
public class OnBeanConditionTypeDeductionFailureTests {
......
......@@ -23,7 +23,7 @@ import org.springframework.beans.factory.BeanCreationException;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration;
......@@ -34,7 +34,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions({ "h2-*.jar", "hsqldb-*.jar" })
public class DataSourceBeanCreationFailureAnalyzerTests {
......
......@@ -26,7 +26,7 @@ import org.junit.runner.RunWith;
import org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext;
import static org.mockito.Mockito.times;
......@@ -37,7 +37,7 @@ import static org.mockito.Mockito.verify;
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("tomcat-jdbc-*.jar")
public class DevToolsEmbeddedDataSourceAutoConfigurationTests
extends AbstractDevToolsDataSourceAutoConfigurationTests {
......
......@@ -22,7 +22,7 @@ import org.junit.runner.RunWith;
import org.springframework.boot.test.rule.OutputCapture;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("spring-web-*.jar")
public class SpringApplicationBuilderExampleTests {
......
......@@ -112,6 +112,12 @@
<optional>true</optional>
</dependency>
<!-- Test -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot</artifactId>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.jetbrains.kotlin</groupId>
<artifactId>kotlin-runtime</artifactId>
......
......@@ -20,7 +20,6 @@ import java.util.List;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.internal.util.MockUtil;
import org.mockito.listeners.InvocationListener;
import org.mockito.listeners.MethodInvocationReport;
import org.mockito.mock.MockCreationSettings;
......@@ -105,9 +104,8 @@ public enum MockReset {
static MockReset get(Object mock) {
MockReset reset = MockReset.NONE;
if (ClassUtils.isPresent("org.mockito.internal.util.MockUtil", null)) {
MockUtil mockUtil = new MockUtil();
if (mockUtil.isMock(mock)) {
MockCreationSettings settings = mockUtil.getMockSettings(mock);
if (Mockito.mockingDetails(mock).isMock()) {
MockCreationSettings settings = SpringBootMockUtil.getMockSettings(mock);
List listeners = settings.getInvocationListeners();
for (Object listener : listeners) {
if (listener instanceof ResetInvocationListener) {
......
......@@ -16,19 +16,14 @@
package org.springframework.boot.test.mock.mockito;
import java.lang.reflect.Field;
import java.util.List;
import org.aopalliance.aop.Advice;
import org.aopalliance.intercept.Interceptor;
import org.aopalliance.intercept.MethodInterceptor;
import org.aopalliance.intercept.MethodInvocation;
import org.mockito.internal.InternalMockHandler;
import org.mockito.internal.matchers.LocalizedMatcher;
import org.mockito.internal.progress.ArgumentMatcherStorage;
import org.mockito.internal.progress.MockingProgress;
import org.mockito.internal.stubbing.InvocationContainer;
import org.mockito.internal.util.MockUtil;
import org.mockito.internal.verification.MockAwareVerificationMode;
import org.mockito.verification.VerificationMode;
......@@ -38,7 +33,6 @@ import org.springframework.aop.support.AopUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.util.AopTestUtils;
import org.springframework.util.Assert;
import org.springframework.util.ReflectionUtils;
/**
* AOP {@link Interceptor} that attempts to make AOP proxy beans work with Mockito. Works
......@@ -58,7 +52,7 @@ class MockitoAopProxyTargetInterceptor implements MethodInterceptor {
MockitoAopProxyTargetInterceptor(Object source, Object target) throws Exception {
this.source = source;
this.target = target;
this.verification = new Verification(target);
this.verification = new Verification();
}
@Override
......@@ -94,21 +88,10 @@ class MockitoAopProxyTargetInterceptor implements MethodInterceptor {
private final Object monitor = new Object();
private final MockingProgress progress;
Verification(Object target) {
MockUtil mockUtil = new MockUtil();
InternalMockHandler<?> handler = mockUtil.getMockHandler(target);
InvocationContainer container = handler.getInvocationContainer();
Field field = ReflectionUtils.findField(container.getClass(),
"mockingProgress");
ReflectionUtils.makeAccessible(field);
this.progress = (MockingProgress) ReflectionUtils.getField(field, container);
}
public boolean isVerifying() {
synchronized (this.monitor) {
VerificationMode mode = this.progress.pullVerificationMode();
VerificationMode mode = SpringBootMockUtil.mockingProgress()
.pullVerificationMode();
if (mode != null) {
resetVerificationStarted(mode);
return true;
......@@ -119,7 +102,8 @@ class MockitoAopProxyTargetInterceptor implements MethodInterceptor {
public void replaceVerifyMock(Object source, Object target) {
synchronized (this.monitor) {
VerificationMode mode = this.progress.pullVerificationMode();
VerificationMode mode = SpringBootMockUtil.mockingProgress()
.pullVerificationMode();
if (mode != null) {
if (mode instanceof MockAwareVerificationMode) {
MockAwareVerificationMode mockAwareMode = (MockAwareVerificationMode) mode;
......@@ -133,12 +117,11 @@ class MockitoAopProxyTargetInterceptor implements MethodInterceptor {
}
private void resetVerificationStarted(VerificationMode mode) {
ArgumentMatcherStorage storage = this.progress.getArgumentMatcherStorage();
ArgumentMatcherStorage storage = SpringBootMockUtil.mockingProgress()
.getArgumentMatcherStorage();
List<LocalizedMatcher> matchers = storage.pullLocalizedMatchers();
this.progress.verificationStarted(mode);
for (LocalizedMatcher matcher : matchers) {
storage.reportMatcher(matcher);
}
SpringBootMockUtil.mockingProgress().verificationStarted(mode);
SpringBootMockUtil.reportMatchers(storage, matchers);
}
}
......
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.mock.mockito;
import java.lang.reflect.Method;
import java.util.List;
import org.mockito.ArgumentMatcher;
import org.mockito.internal.matchers.LocalizedMatcher;
import org.mockito.internal.progress.ArgumentMatcherStorage;
import org.mockito.internal.progress.MockingProgress;
import org.mockito.internal.progress.ThreadSafeMockingProgress;
import org.mockito.internal.util.MockUtil;
import org.mockito.mock.MockCreationSettings;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
/**
* A facade for Mockito's {@link MockUtil} that hides API differences between Mockito 1
* and 2.
*
* @author Andy Wilkinson
*/
class SpringBootMockUtil {
private static final MockUtilAdapter adapter;
static {
if (ClassUtils.isPresent("org.mockito.quality.MockitoHint",
SpringBootMockUtil.class.getClassLoader())) {
adapter = new Mockito2MockUtilAdapter();
}
else {
adapter = new Mockito1MockUtilAdapter();
}
}
static MockCreationSettings<?> getMockSettings(Object mock) {
return adapter.getMockSettings(mock);
}
static MockingProgress mockingProgress() {
return adapter.mockingProgress();
}
static void reportMatchers(ArgumentMatcherStorage storage,
List<LocalizedMatcher> matchers) {
adapter.reportMatchers(storage, matchers);
}
private interface MockUtilAdapter {
MockCreationSettings<?> getMockSettings(Object mock);
MockingProgress mockingProgress();
void reportMatchers(ArgumentMatcherStorage storage,
List<LocalizedMatcher> matchers);
}
private static class Mockito1MockUtilAdapter implements MockUtilAdapter {
private static final MockingProgress mockingProgress = new ThreadSafeMockingProgress();
@Override
public MockCreationSettings<?> getMockSettings(Object mock) {
return new MockUtil().getMockSettings(mock);
}
@Override
public MockingProgress mockingProgress() {
return mockingProgress;
}
@Override
public void reportMatchers(ArgumentMatcherStorage storage,
List<LocalizedMatcher> matchers) {
for (LocalizedMatcher matcher : matchers) {
storage.reportMatcher(matcher);
}
}
}
private static class Mockito2MockUtilAdapter implements MockUtilAdapter {
private final Method getMockSettingsMethod = ReflectionUtils
.findMethod(MockUtil.class, "getMockSettings", Object.class);
private final Method mockingProgressMethod = ReflectionUtils
.findMethod(ThreadSafeMockingProgress.class, "mockingProgress");
private final Method reportMatcherMethod = ReflectionUtils.findMethod(
ArgumentMatcherStorage.class, "reportMatcher", ArgumentMatcher.class);
private final Method getMatcherMethod = ReflectionUtils
.findMethod(LocalizedMatcher.class, "getMatcher");
@Override
public MockCreationSettings<?> getMockSettings(Object mock) {
return (MockCreationSettings<?>) ReflectionUtils
.invokeMethod(this.getMockSettingsMethod, null, mock);
}
@Override
public MockingProgress mockingProgress() {
return (MockingProgress) ReflectionUtils
.invokeMethod(this.mockingProgressMethod, null);
}
@Override
public void reportMatchers(ArgumentMatcherStorage storage,
List<LocalizedMatcher> matchers) {
for (LocalizedMatcher matcher : matchers) {
ReflectionUtils.invokeMethod(this.reportMatcherMethod, storage,
ReflectionUtils.invokeMethod(this.getMatcherMethod, matcher));
}
}
}
}
......@@ -18,7 +18,6 @@ package org.springframework.boot.test.mock.mockito;
import org.mockito.MockSettings;
import org.mockito.Mockito;
import org.mockito.internal.util.MockUtil;
import org.springframework.core.ResolvableType;
import org.springframework.core.style.ToStringCreator;
......@@ -33,8 +32,6 @@ import org.springframework.util.StringUtils;
*/
class SpyDefinition extends Definition {
private MockUtil mockUtil = new MockUtil();
private static final int MULTIPLIER = 31;
private final ResolvableType typeToSpy;
......@@ -87,7 +84,7 @@ class SpyDefinition extends Definition {
public <T> T createSpy(String name, Object instance) {
Assert.notNull(instance, "Instance must not be null");
Assert.isInstanceOf(this.typeToSpy.resolve(), instance);
if (this.mockUtil.isSpy(instance)) {
if (Mockito.mockingDetails(instance).isSpy()) {
return (T) instance;
}
MockSettings settings = MockReset.withSettings(getReset());
......
......@@ -20,7 +20,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Answers;
import org.mockito.internal.util.MockUtil;
import org.mockito.mock.MockCreationSettings;
import org.springframework.boot.test.mock.mockito.example.ExampleExtraInterface;
......@@ -86,7 +85,7 @@ public class MockDefinitionTests {
new Class<?>[] { ExampleExtraInterface.class },
Answers.RETURNS_SMART_NULLS, true, MockReset.BEFORE, null);
ExampleService mock = definition.createMock();
MockCreationSettings<?> settings = new MockUtil().getMockSettings(mock);
MockCreationSettings<?> settings = SpringBootMockUtil.getMockSettings(mock);
assertThat(mock).isInstanceOf(ExampleService.class);
assertThat(mock).isInstanceOf(ExampleExtraInterface.class);
assertThat(settings.getMockName().toString()).isEqualTo("name");
......
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.mock.mockito;
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.springframework.boot.testutil.ClassPathOverrides;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for compatibility with Mockito 2.1
*
* @author Andy Wilkinson
*/
@RunWith(ModifiedClassPathRunner.class)
@ClassPathOverrides("org.mockito:mockito-core:2.1.0")
public class Mockito21Tests {
@Test
public void resetMocksTestExecutionListenerTestsWithMockito2() {
runTests(ResetMocksTestExecutionListenerTests.class);
}
@Test
public void spyBeanWithAopProxyTestsWithMockito2() {
runTests(SpyBeanWithAopProxyTests.class);
}
private void runTests(Class<?> testClass) {
Result result = new JUnitCore().run(testClass);
assertThat(result.getFailureCount()).isEqualTo(0);
assertThat(result.getRunCount()).isGreaterThan(0);
}
}
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.test.mock.mockito;
import org.junit.Test;
import org.junit.runner.JUnitCore;
import org.junit.runner.Result;
import org.junit.runner.RunWith;
import org.springframework.boot.testutil.ClassPathOverrides;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for compatibility with Mockito 2.2
*
* @author Andy Wilkinson
*/
@RunWith(ModifiedClassPathRunner.class)
@ClassPathOverrides("org.mockito:mockito-core:2.2.0")
public class Mockito22Tests {
@Test
public void resetMocksTestExecutionListenerTestsWithMockito2() {
runTests(ResetMocksTestExecutionListenerTests.class);
}
@Test
public void spyBeanWithAopProxyTestsWithMockito2() {
runTests(SpyBeanWithAopProxyTests.class);
}
private void runTests(Class<?> testClass) {
Result result = new JUnitCore().run(testClass);
assertThat(result.getFailureCount()).isEqualTo(0);
assertThat(result.getRunCount()).isGreaterThan(0);
}
}
......@@ -19,7 +19,7 @@ package org.springframework.boot.test.mock.mockito;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.internal.util.MockUtil;
import org.mockito.Mockito;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.annotation.Qualifier;
......@@ -80,7 +80,8 @@ public class MockitoPostProcessorTests {
context.registerBeanDefinition("beanToBeMocked", factoryBeanDefinition);
context.register(MockedFactoryBean.class);
context.refresh();
assertThat(new MockUtil().isMock(context.getBean("beanToBeMocked"))).isTrue();
assertThat(Mockito.mockingDetails(context.getBean("beanToBeMocked")).isMock())
.isTrue();
}
@Configuration
......
......@@ -20,7 +20,6 @@ import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Answers;
import org.mockito.internal.util.MockUtil;
import org.mockito.mock.MockCreationSettings;
import org.springframework.boot.test.mock.mockito.example.ExampleService;
......@@ -79,7 +78,7 @@ public class SpyDefinitionTests {
SpyDefinition definition = new SpyDefinition("name", REAL_SERVICE_TYPE,
MockReset.BEFORE, true, null);
RealExampleService spy = definition.createSpy(new RealExampleService("hello"));
MockCreationSettings<?> settings = new MockUtil().getMockSettings(spy);
MockCreationSettings<?> settings = SpringBootMockUtil.getMockSettings(spy);
assertThat(spy).isInstanceOf(ExampleService.class);
assertThat(settings.getMockName().toString()).isEqualTo("name");
assertThat(settings.getDefaultAnswer())
......
......@@ -290,6 +290,64 @@
<groupId>org.apache.httpcomponents</groupId>
<artifactId>httpasyncclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-aether-provider</artifactId>
<exclusions>
<exclusion>
<artifactId>org.eclipse.sisu.plexus</artifactId>
<groupId>org.eclipse.sisu</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.apache.maven</groupId>
<artifactId>maven-settings-builder</artifactId>
</dependency>
<dependency>
<groupId>org.codehaus.plexus</groupId>
<artifactId>plexus-component-api</artifactId>
<exclusions>
<exclusion>
<groupId>*</groupId>
<artifactId>*</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-api</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-connector-basic</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-impl</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-spi</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-file</artifactId>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-transport-http</artifactId>
<exclusions>
<exclusion>
<artifactId>jcl-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.eclipse.aether</groupId>
<artifactId>aether-util</artifactId>
</dependency>
<dependency>
<groupId>org.firebirdsql.jdbc</groupId>
......
......@@ -23,7 +23,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.diagnostics.FailureAnalysis;
import org.springframework.boot.testutil.ClassPathExclusions;
import org.springframework.boot.testutil.FilteredClassPathRunner;
import org.springframework.boot.testutil.ModifiedClassPathRunner;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import static org.assertj.core.api.Assertions.assertThat;
......@@ -34,7 +34,7 @@ import static org.junit.Assert.fail;
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("hibernate-validator-*.jar")
public class ValidationExceptionFailureAnalyzerTests {
......
......@@ -23,7 +23,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used in combination with {@link FilteredClassPathRunner} to exclude entries
* Annotation used in combination with {@link ModifiedClassPathRunner} to exclude entries
* from the classpath.
*
* @author Andy Wilkinson
......
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testutil;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* Annotation used in combination with {@link ModifiedClassPathRunner} to override entries
* on the classpath.
*
* @author Andy Wilkinson
*/
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
public @interface ClassPathOverrides {
/**
* One or more sets of Maven coordinates ({@code groupId:artifactId:version}) to be
* added to the classpath. The additions will take precedence over any existing
* classes on the classpath.
* @return the coordinates
*/
String[] value();
}
......@@ -28,6 +28,22 @@ import java.util.List;
import java.util.jar.Attributes;
import java.util.jar.JarFile;
import org.apache.maven.repository.internal.MavenRepositorySystemUtils;
import org.eclipse.aether.DefaultRepositorySystemSession;
import org.eclipse.aether.RepositorySystem;
import org.eclipse.aether.artifact.DefaultArtifact;
import org.eclipse.aether.collection.CollectRequest;
import org.eclipse.aether.connector.basic.BasicRepositoryConnectorFactory;
import org.eclipse.aether.graph.Dependency;
import org.eclipse.aether.impl.DefaultServiceLocator;
import org.eclipse.aether.repository.LocalRepository;
import org.eclipse.aether.repository.RemoteRepository;
import org.eclipse.aether.resolution.ArtifactResult;
import org.eclipse.aether.resolution.DependencyRequest;
import org.eclipse.aether.resolution.DependencyResult;
import org.eclipse.aether.spi.connector.RepositoryConnectorFactory;
import org.eclipse.aether.spi.connector.transport.TransporterFactory;
import org.eclipse.aether.transport.http.HttpTransporterFactory;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;
......@@ -38,16 +54,17 @@ import org.springframework.util.AntPathMatcher;
import org.springframework.util.StringUtils;
/**
* A custom {@link BlockJUnit4ClassRunner} that runs tests using a filtered class path.
* Entries are excluded from the class path using {@link ClassPathExclusions} on the test
* class. A class loader is created with the customized class path and is used both to
* load the test class and as the thread context class loader while the test is being run.
* A custom {@link BlockJUnit4ClassRunner} that runs tests using a modified class path.
* Entries are excluded from the class path using {@link ClassPathExclusions} and
* overridden using {@link ClassPathOverrides} on the test class. A class loader is
* created with the customized class path and is used both to load the test class and as
* the thread context class loader while the test is being run.
*
* @author Andy Wilkinson
*/
public class FilteredClassPathRunner extends BlockJUnit4ClassRunner {
public class ModifiedClassPathRunner extends BlockJUnit4ClassRunner {
public FilteredClassPathRunner(Class<?> testClass) throws InitializationError {
public ModifiedClassPathRunner(Class<?> testClass) throws InitializationError {
super(testClass);
}
......@@ -64,7 +81,7 @@ public class FilteredClassPathRunner extends BlockJUnit4ClassRunner {
private URLClassLoader createTestClassLoader(Class<?> testClass) throws Exception {
URLClassLoader classLoader = (URLClassLoader) this.getClass().getClassLoader();
return new FilteredClassLoader(filterUrls(extractUrls(classLoader), testClass),
return new FilteredClassLoader(processUrls(extractUrls(classLoader), testClass),
classLoader.getParent(), classLoader);
}
......@@ -104,15 +121,61 @@ public class FilteredClassPathRunner extends BlockJUnit4ClassRunner {
}
}
private URL[] filterUrls(URL[] urls, Class<?> testClass) throws Exception {
private URL[] processUrls(URL[] urls, Class<?> testClass) throws Exception {
ClassPathEntryFilter filter = new ClassPathEntryFilter(testClass);
List<URL> filteredUrls = new ArrayList<URL>();
List<URL> processedUrls = new ArrayList<URL>();
processedUrls.addAll(getAdditionalUrls(testClass));
for (URL url : urls) {
if (!filter.isExcluded(url)) {
filteredUrls.add(url);
processedUrls.add(url);
}
}
return filteredUrls.toArray(new URL[filteredUrls.size()]);
return processedUrls.toArray(new URL[processedUrls.size()]);
}
private List<URL> getAdditionalUrls(Class<?> testClass) throws Exception {
ClassPathOverrides overrides = AnnotationUtils.findAnnotation(testClass,
ClassPathOverrides.class);
if (overrides == null) {
return Collections.emptyList();
}
return resolveCoordinates(overrides.value());
}
private List<URL> resolveCoordinates(String[] coordinates) throws Exception {
DefaultServiceLocator serviceLocator = MavenRepositorySystemUtils
.newServiceLocator();
serviceLocator.addService(RepositoryConnectorFactory.class,
BasicRepositoryConnectorFactory.class);
serviceLocator.addService(TransporterFactory.class, HttpTransporterFactory.class);
RepositorySystem repositorySystem = serviceLocator
.getService(RepositorySystem.class);
DefaultRepositorySystemSession session = MavenRepositorySystemUtils.newSession();
LocalRepository localRepository = new LocalRepository(
System.getProperty("user.home") + "/.m2/repository");
session.setLocalRepositoryManager(
repositorySystem.newLocalRepositoryManager(session, localRepository));
CollectRequest collectRequest = new CollectRequest(null,
Arrays.asList(new RemoteRepository.Builder("central", "default",
"http://central.maven.org/maven2").build()));
collectRequest.setDependencies(createDependencies(coordinates));
DependencyRequest dependencyRequest = new DependencyRequest(collectRequest, null);
DependencyResult result = repositorySystem.resolveDependencies(session,
dependencyRequest);
List<URL> resolvedArtifacts = new ArrayList<URL>();
for (ArtifactResult artifact : result.getArtifactResults()) {
resolvedArtifacts.add(artifact.getArtifact().getFile().toURI().toURL());
}
return resolvedArtifacts;
}
private List<Dependency> createDependencies(String[] allCoordinates) {
List<Dependency> dependencies = new ArrayList<Dependency>();
for (String coordinates : allCoordinates) {
dependencies.add(new Dependency(new DefaultArtifact(coordinates), null));
}
return dependencies;
}
/**
......
......@@ -22,13 +22,13 @@ import org.junit.runner.RunWith;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link FilteredClassPathRunner}
* Tests for {@link ModifiedClassPathRunner} excluding entries from the class path.
*
* @author Andy Wilkinson
*/
@RunWith(FilteredClassPathRunner.class)
@RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions("hibernate-validator-*.jar")
public class FilteredClassPathRunnerTests {
public class ModifiedClassPathRunnerExclusionsTests {
private static final String EXCLUDED_RESOURCE = "META-INF/services/"
+ "javax.validation.spi.ValidationProvider";
......
/*
* Copyright 2012-2016 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.boot.testutil;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.context.ApplicationContext;
import org.springframework.util.StringUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link ModifiedClassPathRunner} overriding entries on the class path.
*
* @author Andy Wilkinson
*/
@RunWith(ModifiedClassPathRunner.class)
@ClassPathOverrides("org.springframework:spring-context:4.1.0.RELEASE")
public class ModifiedClassPathRunnerOverridesTests {
@Test
public void classesAreLoadedFromOverride() {
assertThat(ApplicationContext.class.getProtectionDomain().getCodeSource()
.getLocation().toString()).endsWith("spring-context-4.1.0.RELEASE.jar");
}
@Test
public void classesAreLoadedFromTransitiveDependencyOfOverride() {
assertThat(StringUtils.class.getProtectionDomain().getCodeSource().getLocation()
.toString()).endsWith("spring-core-4.1.0.RELEASE.jar");
}
}
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment