org.xwiki.test.isolation.IsolatedTestRunner Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of xwiki-commons-tool-test-simple Show documentation
Show all versions of xwiki-commons-tool-test-simple Show documentation
For tests not requiring notion of Components
/*
* See the NOTICE file distributed with this work for additional
* information regarding copyright ownership.
*
* This is free software; you can redistribute it and/or modify it
* under the terms of the GNU Lesser General Public License as
* published by the Free Software Foundation; either version 2.1 of
* the License, or (at your option) any later version.
*
* This software is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
* Lesser General Public License for more details.
*
* You should have received a copy of the GNU Lesser General Public
* License along with this software; if not, write to the Free
* Software Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA
* 02110-1301 USA, or see the FSF site: http://www.fsf.org.
*/
package org.xwiki.test.isolation;
import java.net.URLClassLoader;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.model.InitializationError;
/**
* Specialized JUnit4 runner to isolate some classes during the execution of a test class.
* This runner should be used in combination with the {@link IsolatedClassPrefix} annotation.
*
* Isolation is helpful when you do not want to pollute the application ClassLoader with some
* classes under test, or some dynamically loaded classes during a test. It could be used to
* reinitialize statics and drop out those dynamically loaded classes after the test.
*
* To use this class, define a JUnit {@code @RunWith} annotation on your test class and also
* add a {@link IsolatedClassPrefix} annotation to define the list of class prefixes that should
* be isolated from the rest of your tests.
*
* For example:
*
{@code
* @RunWith(IsolatedTestRunner)
* @IsolatedClassPrefix("org.xwiki.mypackage")
* public class MyPackageTest
* {
* @Test
* public void someTest() throws Exception
* {
* ...
* }
* ...
* }
* }
*
* The prefixes should at least include a prefix that match your test class, else the initialization will fail,
* since your test would not be run in isolation properly.
*
* If you are mocking some of your isolated classes with Mockito in different tests (either isolated or not), you
* will need to disable the class cache used by Mockito to avoid ClassCastException during mocking. You can disable
* the cache by adding the following class to your test Jar:
*
* {@code
* package org.mockito.configuration;
* public class MockitoConfiguration extends DefaultMockitoConfiguration
* {
* @Override
* public boolean enableClassCache()
* {
* return false;
* }
* }
* }
*
* @version $Id: 889edd6c6c36cdfff01b20af1080385d3003246f $
* @since 5.0M2
*/
public class IsolatedTestRunner extends BlockJUnit4ClassRunner
{
/**
* Creates a BlockJUnit4ClassRunner to run {@code clazz}.
*
* @param clazz the test class to be run in isolation
* @throws InitializationError if the test class is malformed.
*/
public IsolatedTestRunner(Class> clazz) throws InitializationError
{
super(getFromTestClassloader(clazz));
}
/**
* @param clazz the test class to be run in isolation
* @return an isolated version of the test class, using an separated ClassLoader.
* @throws InitializationError if the test class is malformed.
*/
private static Class> getFromTestClassloader(Class> clazz) throws InitializationError
{
String name = clazz.getName();
IsolatedClassPrefix isolatedClassPrefix = clazz.getAnnotation(IsolatedClassPrefix.class);
if (isolatedClassPrefix == null) {
throw new InitializationError("To run test with some classes isolated, you need to define the prefix of "
+ "these classes using annotation @IsolatedClassPrefix "
+ "(ie: @IsolatedClassPrefix(\"org.xwiki.mymodule\").");
}
String[] prefixes = isolatedClassPrefix.value();
StringBuilder prefixList = null;
boolean classMatched = false;
for (String prefix : prefixes) {
if (name.startsWith(prefix)) {
classMatched = true;
}
if (prefixList == null) {
prefixList = new StringBuilder(prefix);
} else {
prefixList.append(", ").append(prefix);
}
}
if (!classMatched) {
throw new InitializationError(String.format("To run test with some classes isolated, your test class "
+ "should be itself isolated, and therefore, should part of the class prefix used for isolation. "
+ "Your class [%s] does not match any prefixes in [%s]", name, prefixList));
}
try {
ClassLoader reloadRightClassLoader = new IsolatedTestClassLoader(prefixes);
return Class.forName(name, true, reloadRightClassLoader);
} catch (ClassNotFoundException e) {
throw new InitializationError(e);
}
}
/**
* A ClassLoader implementation that loads itself classes based on given prefixes, and delegate the rest to
* its parent ClassLoader.
*/
private static class IsolatedTestClassLoader extends URLClassLoader
{
/**
* List of class prefixes isolated by this ClassLoader.
*/
private final String[] prefixes;
/**
* Creates a IsolatedTestClassLoader for the provided prefixes.
* @param prefixes List of class name prefix use to limit isolation to the given classes.
*/
IsolatedTestClassLoader(String[] prefixes) {
super(((URLClassLoader) Thread.currentThread().getContextClassLoader()).getURLs());
this.prefixes = prefixes;
}
@Override
public Class> loadClass(String name) throws ClassNotFoundException {
for (String prefix : prefixes) {
if (name.startsWith(prefix)) {
return loadLocalClass(name);
}
}
return super.loadClass(name);
}
private Class> loadLocalClass(String name) throws ClassNotFoundException
{
synchronized (getClassLoadingLock(name)) {
// First, check if the class has already been loaded
Class> c = findLoadedClass(name);
if (c == null) {
c = findClass(name);
}
return c;
}
}
}
}