├── pubring.gpg ├── secring.gpg.enc ├── src ├── test │ └── java │ │ └── com │ │ └── binarytweed │ │ └── test │ │ ├── ReferencedClass.java │ │ ├── ReferencingClass.java │ │ ├── QuarantinedRunnerIntegrationTest.java │ │ ├── AnnotationInheritanceTest.java │ │ ├── QuarantinedPatternDiscovererTest.java │ │ ├── DelegateRunningToDiscovererTest.java │ │ ├── QuarantiningUrlClassLoaderTest.java │ │ └── ReferencingTest.java └── main │ └── java │ └── com │ └── binarytweed │ └── test │ ├── DelegateRunningToDiscoverer.java │ ├── QuarantinedPatternDiscoverer.java │ ├── Quarantine.java │ ├── DelegateRunningTo.java │ ├── QuarantiningUrlClassLoader.java │ └── QuarantiningRunner.java ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md └── pom.xml /pubring.gpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryTweed/quarantining-test-runner/HEAD/pubring.gpg -------------------------------------------------------------------------------- /secring.gpg.enc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/BinaryTweed/quarantining-test-runner/HEAD/secring.gpg.enc -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/ReferencedClass.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | public class ReferencedClass 4 | { 5 | public static int VALUE = 0; 6 | } 7 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.class 2 | target/ 3 | 4 | # IntelliJ IDEA 5 | .idea/ 6 | *.iml 7 | 8 | # Eclipse 9 | .classpath 10 | .project 11 | .settings/ 12 | 13 | # Mobile Tools for Java (J2ME) 14 | .mtj.tmp/ 15 | 16 | # Package Files # 17 | *.jar 18 | *.war 19 | *.ear 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/ReferencingClass.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | 4 | public class ReferencingClass 5 | { 6 | public final ReferencedClass referenced; 7 | 8 | 9 | public ReferencingClass() 10 | { 11 | referenced = new ReferencedClass(); 12 | } 13 | 14 | 15 | public String getReferencedMember() 16 | { 17 | return String.valueOf(ReferencedClass.VALUE); 18 | } 19 | 20 | 21 | public ClassLoader getReferencedClassLoader() 22 | { 23 | return ReferencedClass.class.getClassLoader(); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/DelegateRunningToDiscoverer.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import org.junit.runner.Runner; 4 | import org.junit.runners.JUnit4; 5 | 6 | public class DelegateRunningToDiscoverer 7 | { 8 | public Class getDelegateRunningToOn(Class testClass) 9 | { 10 | Class runnerClass = JUnit4.class; 11 | DelegateRunningTo annotation = testClass.getAnnotation(DelegateRunningTo.class); 12 | 13 | if(annotation != null) 14 | { 15 | runnerClass = annotation.value(); 16 | } 17 | 18 | return runnerClass; 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/QuarantinedPatternDiscoverer.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | 4 | public class QuarantinedPatternDiscoverer 5 | { 6 | /** 7 | * 8 | * @param annotatedClass class on which to look for {@code Quarantine} annotation 9 | * @return values specified on the {@code Quarantine} annotation 10 | */ 11 | public String[] getQuarantinedPatternsOn(Class annotatedClass) 12 | { 13 | Quarantine annotation = annotatedClass.getAnnotation(Quarantine.class); 14 | 15 | if(annotation != null) 16 | { 17 | return annotation.value(); 18 | } 19 | 20 | return new String[]{}; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/Quarantine.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Inherited; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | /** 10 | * Defines prefix patterns which {@code QuarantiningUrlClassLoader} will load 11 | * in a separate ClassLoader. 12 | * 13 | */ 14 | @Retention(RetentionPolicy.RUNTIME) 15 | @Target(ElementType.TYPE) 16 | @Inherited 17 | public @interface Quarantine 18 | { 19 | /** 20 | * 21 | * @return prefix patterns which {@code QuarantiningUrlClassLoader} will load in a separate ClassLoader 22 | */ 23 | public String[] value() default {}; 24 | } 25 | -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/DelegateRunningTo.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Inherited; 5 | import java.lang.annotation.Retention; 6 | import java.lang.annotation.RetentionPolicy; 7 | import java.lang.annotation.Target; 8 | 9 | import org.junit.runner.Runner; 10 | import org.junit.runners.JUnit4; 11 | 12 | /** 13 | * Specifies the {@code Runner} that {@code QuarantiningRunner} delegates to. 14 | * 15 | */ 16 | @Retention(RetentionPolicy.RUNTIME) 17 | @Target(ElementType.TYPE) 18 | @Inherited 19 | public @interface DelegateRunningTo 20 | { 21 | /** 22 | * @return the {@code Runner} that {@code QuarantiningRunner} delegates to. 23 | */ 24 | public Class value() default JUnit4.class; 25 | } 26 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: java 2 | branches: 3 | except: 4 | - travis 5 | before_install: 6 | - openssl aes-256-cbc -K $encrypted_ff613480c5bd_key -iv $encrypted_ff613480c5bd_iv 7 | -in secring.gpg.enc -out secring.gpg -d 8 | after_success: 9 | - "git clone -b travis `git config --get remote.origin.url` target/travis" 10 | - "mvn deploy -Dgpg.passphrase=$GPG_PASSPHRASE --settings target/travis/settings.xml" 11 | env: 12 | global: 13 | - secure: ZeW0ELyKTtfbuqyIxu+2ZsitDZHYg9pgpSIUzmqtQmtDFTF3LPZT3N1Echs9OQ80DnHamXIXlZBWBxfki/XmJvwNVe94+w+PZZIoZ5S8qB233hIX47wz0kyn7qDd4552/xXqBATtk0EXKsWw8ovoWIhuGNKZ0RsT0FNBp2HoiPA= 14 | - secure: FFfI9uILcw+HeCNHH53jUzZl3tkKsqM8Te5FAJJpUA2xeLlfRMs7vNHr85cBNvu312gqXLBTllkRwVs1GPx09OEplHJWfRiUR/4abo3pW49GNUK8Z1Le3ugxPWac5Bw+M+Ey7vOYsUQfKQXpoGhCq+G3v3TDgfDJfgF2DM1yNTU= 15 | - secure: QyGwgBLky2ziXsz5rXNbnC9Exv6/8VdCnWoOJOozY5ooFPQLa1BQHZaI4q0vASIcXqtFhy06Tpb19MDaUynd0gWCfDmXTgvxKb+DlDm4ICkrj9KMGiMAP8oBjNUvpjJ7/Xgm24KqitBpMDxvINOXFsNqkIgxtuJ89gXTaYPx6AU= 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Binary Tweed 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | 23 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/QuarantinedRunnerIntegrationTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import org.junit.Test; 7 | import org.junit.runner.RunWith; 8 | 9 | @RunWith(QuarantiningRunner.class) 10 | @Quarantine("com.binarytweed.test.ReferencingClass") 11 | public class QuarantinedRunnerIntegrationTest 12 | { 13 | @Test 14 | public void classLoadedByQuarantinedUrlClassLoader() 15 | { 16 | assertThat(getClass().getClassLoader(), instanceOf(QuarantiningUrlClassLoader.class)); 17 | } 18 | 19 | 20 | @Test 21 | public void unspecifiedClassNotLoadedByQuarantinedUrlClassLoader() 22 | { 23 | Integer myInteger = new Integer(117); 24 | assertThat("Class not quarantined should be loaded by default classloader, which returns as null", myInteger.getClass().getClassLoader(), nullValue()); 25 | } 26 | 27 | 28 | @Test 29 | public void specifiedClassLoadedByQuarantinedUrlClassLoader() 30 | { 31 | ReferencingClass instance = new ReferencingClass(); 32 | assertThat(instance.getClass().getClassLoader(), instanceOf(QuarantiningUrlClassLoader.class)); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/AnnotationInheritanceTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.equalTo; 4 | import static org.hamcrest.Matchers.is; 5 | import static org.hamcrest.Matchers.notNullValue; 6 | import static org.junit.Assert.assertThat; 7 | 8 | import org.junit.Test; 9 | 10 | public class AnnotationInheritanceTest 11 | { 12 | @Quarantine({"com.foo.bar"}) 13 | @DelegateRunningTo(QuarantiningRunner.class) 14 | public static class AbstractQuarantineTest 15 | { 16 | } 17 | 18 | public static class ConcreteQuarantineTest extends AbstractQuarantineTest 19 | { 20 | } 21 | 22 | 23 | @Test 24 | @SuppressWarnings("rawtypes") 25 | public void extendedTestInheritsAnnotations() 26 | { 27 | DelegateRunningTo delegateRunningTo = ConcreteQuarantineTest.class.getAnnotation(DelegateRunningTo.class); 28 | Quarantine quarantine = ConcreteQuarantineTest.class.getAnnotation(Quarantine.class); 29 | 30 | assertThat(delegateRunningTo, notNullValue()); 31 | assertThat(delegateRunningTo.value(), equalTo((Class) QuarantiningRunner.class)); 32 | 33 | assertThat(quarantine, notNullValue()); 34 | assertThat(quarantine.value(), is(new String[]{"com.foo.bar"})); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/QuarantinedPatternDiscovererTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import org.junit.Test; 7 | 8 | import com.binarytweed.test.Quarantine; 9 | import com.binarytweed.test.QuarantinedPatternDiscoverer; 10 | 11 | public class QuarantinedPatternDiscovererTest 12 | { 13 | @Quarantine({"java.lang.Object", "com.binarytweed"}) 14 | public static class Fixture 15 | { 16 | } 17 | 18 | 19 | public static class UnannotatedFixture 20 | { 21 | } 22 | 23 | 24 | @Test 25 | public void getQuarantinedPatterns() 26 | { 27 | QuarantinedPatternDiscoverer discoverer = new QuarantinedPatternDiscoverer(); 28 | String[] discovered = discoverer.getQuarantinedPatternsOn(Fixture.class); 29 | assertThat(discovered, arrayContaining("java.lang.Object", "com.binarytweed")); 30 | } 31 | 32 | 33 | @Test 34 | public void unannotatedClassYieldsEmptyArray() 35 | { 36 | QuarantinedPatternDiscoverer discoverer = new QuarantinedPatternDiscoverer(); 37 | String[] discovered = discoverer.getQuarantinedPatternsOn(UnannotatedFixture.class); 38 | assertThat(discovered, isA(String[].class)); 39 | assertThat(discovered.length, is(0)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/DelegateRunningToDiscovererTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import org.junit.Test; 7 | import org.junit.runners.JUnit4; 8 | 9 | @SuppressWarnings("rawtypes") 10 | public class DelegateRunningToDiscovererTest 11 | { 12 | @DelegateRunningTo(QuarantiningRunner.class) 13 | public static class FixtureWithValue 14 | { 15 | } 16 | 17 | 18 | @DelegateRunningTo 19 | public static class FixtureWithoutValue 20 | { 21 | } 22 | 23 | 24 | public static class UnannotatedFixture 25 | { 26 | } 27 | 28 | 29 | @Test 30 | public void annotatedClassWithValueReturnsSpecifiedValue() 31 | { 32 | DelegateRunningToDiscoverer discoverer = new DelegateRunningToDiscoverer(); 33 | Class discovered = discoverer.getDelegateRunningToOn(FixtureWithValue.class); 34 | assertThat(discovered, equalTo((Class) QuarantiningRunner.class)); 35 | } 36 | 37 | 38 | @Test 39 | public void annotatedClassWithoutValueReturnsDefault() 40 | { 41 | DelegateRunningToDiscoverer discoverer = new DelegateRunningToDiscoverer(); 42 | Class discovered = discoverer.getDelegateRunningToOn(FixtureWithoutValue.class); 43 | assertThat(discovered, equalTo((Class) JUnit4.class)); 44 | } 45 | 46 | 47 | @Test 48 | public void unannotatedClassReturnsDefault() 49 | { 50 | DelegateRunningToDiscoverer discoverer = new DelegateRunningToDiscoverer(); 51 | Class discovered = discoverer.getDelegateRunningToOn(UnannotatedFixture.class); 52 | assertThat(discovered, equalTo((Class) JUnit4.class)); 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/QuarantiningUrlClassLoaderTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import org.junit.Test; 7 | 8 | @SuppressWarnings({"resource", "unchecked"}) 9 | public class QuarantiningUrlClassLoaderTest 10 | { 11 | @Test 12 | public void quarantinedClassIsLoadedByUs() throws Exception 13 | { 14 | QuarantiningUrlClassLoader loader = new QuarantiningUrlClassLoader("com.binarytweed"); 15 | Class quarantinedClass = loader.loadClass("com.binarytweed.test.ReferencingClass"); 16 | assertThat(quarantinedClass.getClassLoader(), equalTo((ClassLoader) loader)); 17 | } 18 | 19 | 20 | @Test 21 | public void otherClassIsLoadedByParent() throws Exception 22 | { 23 | QuarantiningUrlClassLoader loader = new QuarantiningUrlClassLoader("com.binarytweed"); 24 | Class otherClass = loader.loadClass("java.sql.Timestamp"); 25 | assertThat(otherClass.getClassLoader(), nullValue()); 26 | } 27 | 28 | 29 | @Test 30 | public void settingStaticVarBeforeIsolatedLoad() throws Exception 31 | { 32 | ReferencedClass.VALUE = 100; 33 | ReferencingClass instance = new ReferencingClass(); 34 | 35 | QuarantiningUrlClassLoader loader = new QuarantiningUrlClassLoader(ReferencingClass.class.getName(), ReferencedClass.class.getName()); 36 | 37 | 38 | Class isolated = (Class) loader.loadClass(ReferencingClass.class.getName()); 39 | 40 | assertThat((Class) instance.getClass(), not(equalTo(isolated))); 41 | assertThat((String) isolated.getDeclaredMethod("getReferencedMember").invoke(isolated.newInstance()), is("0")); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # quarantining-test-runner 2 | 3 | [![Build Status](https://travis-ci.org/BinaryTweed/quarantining-test-runner.svg)](https://travis-ci.org/BinaryTweed/quarantining-test-runner) 4 | 5 | A JUnit test runner that loads the test class and optionally specified classes in a separate `ClassLoader`. This means that static members will effectively be reset between each test class, as new versions of those classes are loaded. 6 | 7 | ## Maven dependency 8 | 9 | ```xml 10 | 11 | com.binarytweed 12 | quarantining-test-runner 13 | 0.0.3 14 | 15 | ``` 16 | 17 | Check the latest version on [Maven Central](http://search.maven.org/#search%7Cga%7C1%7Cg%3A%22com.binarytweed%22%20a%3A%22quarantining-test-runner%22). 18 | 19 | 20 | ## To use 21 | 22 | ```java 23 | import com.binarytweed.test.Quarantine; 24 | import com.binarytweed.test.QuarantiningRunner; 25 | 26 | @RunWith(QuarantiningRunner.class) 27 | @Quarantine({"com.binarytweed", "com.example.ClassName"}) 28 | public class MyIsolatedTest { 29 | ... 30 | ``` 31 | 32 | 1. Annotate your test class with `@RunWith(QuarantiningRunner.class)`. 33 | 1. Use `@Quarantine("com.example")` to specify patterns which separately-loaded class names should start with. 34 | 1. Optionally specify `@DelegateRunningTo(SomeCustomRunner.class)` to have `QuarantiningRunner` use another `Runner` implementation. By default it uses [`org.junit.runners.JUnit4`](http://junit.sourceforge.net/javadoc/org/junit/runners/JUnit4.html) (which currently extends `BlockJUnit4ClassRunner`). 35 | 36 | ## How it works 37 | 38 | The test class and runner class are loaded using `QuarantiningUrlClassLoader`. When either of these two classes reference a class that hasn't yet been loaded, `QuarantiningUrlClassLoader` will be used to load the requested class. 39 | 40 | It will delegate loading classes that _do not match_ the patterns provided by `@Quarantined` to its parent `ClassLoader`; patterns that _do match_ are loaded by itself. Because each test run uses a a distinct instance of `QuarantiningUrlClassLoader`, any classes it loads are distinct from any previous runs. -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/QuarantiningUrlClassLoader.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import java.net.URLClassLoader; 4 | import java.util.HashSet; 5 | import java.util.Set; 6 | 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | 10 | 11 | /** 12 | * If a class name starts with any of the supplied patterns, it is loaded by 13 | * this classloader; otherwise it is loaded by the parent classloader. 14 | * 15 | */ 16 | public class QuarantiningUrlClassLoader extends URLClassLoader 17 | { 18 | private static final Logger logger = LoggerFactory.getLogger(QuarantiningUrlClassLoader.class); 19 | 20 | 21 | private final Set quarantinedClassNames; 22 | 23 | 24 | /** 25 | * 26 | * @param quarantinedClassNames prefixes to match against when deciding how to load a class 27 | */ 28 | public QuarantiningUrlClassLoader(String... quarantinedClassNames) 29 | { 30 | super(((URLClassLoader) getSystemClassLoader()).getURLs()); 31 | logger.trace("[{}] was loaded by [{}]", getClass().getName(), getClass().getClassLoader()); 32 | 33 | this.quarantinedClassNames = new HashSet<>(); 34 | for(String className : quarantinedClassNames) 35 | { 36 | this.quarantinedClassNames.add(className); 37 | } 38 | } 39 | 40 | 41 | /** 42 | * If a class name starts with any of the supplied patterns, it is loaded by 43 | * this classloader; otherwise it is loaded by the parent classloader. 44 | * 45 | * @param name class to load 46 | */ 47 | @Override 48 | public Class loadClass(String name) throws ClassNotFoundException 49 | { 50 | boolean quarantine = false; 51 | 52 | for(String quarantinedPattern : quarantinedClassNames) 53 | { 54 | if(name.startsWith(quarantinedPattern)) 55 | { 56 | quarantine = true; 57 | break; 58 | } 59 | } 60 | 61 | if(quarantine) 62 | { 63 | logger.debug("Detected quarantined class [{}]", name); 64 | try 65 | { 66 | return findClass(name); 67 | } 68 | catch (ClassNotFoundException e) 69 | { 70 | logger.error("Could not load [{}]", name); 71 | throw e; 72 | } 73 | } 74 | 75 | logger.trace("[{}] being loaded by parent", name); 76 | return super.loadClass(name); 77 | } 78 | } -------------------------------------------------------------------------------- /src/test/java/com/binarytweed/test/ReferencingTest.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import static org.hamcrest.Matchers.*; 4 | import static org.junit.Assert.*; 5 | 6 | import java.net.URL; 7 | import java.net.URLClassLoader; 8 | 9 | import org.junit.Test; 10 | 11 | /** 12 | * Here to prove how classloaders actualy work! 13 | * 14 | */ 15 | @SuppressWarnings({"resource", "unchecked"}) 16 | public class ReferencingTest 17 | { 18 | @Test 19 | public void settingStaticVarBeforeIsolatedLoad() throws Exception 20 | { 21 | ReferencedClass.VALUE = 100; 22 | ReferencingClass instance = new ReferencingClass(); 23 | 24 | URL[] systemUrls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(); 25 | URLClassLoader loader = new URLClassLoader(systemUrls, null); 26 | 27 | Class isolated = (Class) loader.loadClass("com.binarytweed.test.ReferencingClass"); 28 | 29 | assertThat((Class) instance.getClass(), not(equalTo(isolated))); 30 | 31 | assertThat((String) isolated.getDeclaredMethod("getReferencedMember").invoke(isolated.newInstance()), is("0")); 32 | } 33 | 34 | 35 | @Test 36 | public void settingStaticVarAfterIsolatedLoad() throws Exception 37 | { 38 | URL[] systemUrls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(); 39 | URLClassLoader loader = new URLClassLoader(systemUrls, null); 40 | 41 | Class isolated = (Class) loader.loadClass("com.binarytweed.test.ReferencingClass"); 42 | assertThat((String) isolated.getDeclaredMethod("getReferencedMember").invoke(isolated.newInstance()), is("0")); 43 | 44 | ReferencedClass.VALUE = 100; 45 | assertThat((String) isolated.getDeclaredMethod("getReferencedMember").invoke(isolated.newInstance()), is("0")); 46 | } 47 | 48 | 49 | @Test 50 | public void classLoaderOfDeclaredClassIsDifferent() throws Exception 51 | { 52 | ReferencingClass instance = new ReferencingClass(); 53 | 54 | URL[] systemUrls = ((URLClassLoader) ClassLoader.getSystemClassLoader()).getURLs(); 55 | URLClassLoader loader = new URLClassLoader(systemUrls, null); 56 | 57 | Class isolated = (Class) loader.loadClass("com.binarytweed.test.ReferencingClass"); 58 | 59 | assertThat((ClassLoader) isolated.getDeclaredMethod("getReferencedClassLoader").invoke(isolated.newInstance()), not(instance.getReferencedClassLoader())); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/java/com/binarytweed/test/QuarantiningRunner.java: -------------------------------------------------------------------------------- 1 | package com.binarytweed.test; 2 | 3 | import java.lang.reflect.InvocationTargetException; 4 | import java.util.Arrays; 5 | 6 | import org.junit.runner.Description; 7 | import org.junit.runner.Runner; 8 | import org.junit.runner.notification.Failure; 9 | import org.junit.runner.notification.RunNotifier; 10 | import org.junit.runners.model.InitializationError; 11 | 12 | /** 13 | * Uses {@code QuarantiningUrlClassLoader} to load the test class, meaning the 14 | * {@code Quarantine} annotation can be used to ensure certain classes are 15 | * loaded separately. 16 | * 17 | * Use of a separate class loader allows classes to be reloaded for each test 18 | * class, which is handy when you're testing frameworks that make use of static 19 | * members. 20 | * 21 | * The selective quarantining is required because if the test class and its 22 | * 'children' are all loaded by a different class loader, then the {@code Test} 23 | * annotations yield different {@code Class} instances. JUnit then thinks there 24 | * are no runnable methods, because it looks them up by Class. 25 | */ 26 | public class QuarantiningRunner extends Runner 27 | { 28 | private final Object innerRunner; 29 | private final Class innerRunnerClass; 30 | private final DelegateRunningToDiscoverer delegateRunningToDiscoverer; 31 | private final QuarantinedPatternDiscoverer quarantinedPatternDiscoverer; 32 | 33 | 34 | public QuarantiningRunner(Class testFileClass) throws InitializationError 35 | { 36 | delegateRunningToDiscoverer = new DelegateRunningToDiscoverer(); 37 | quarantinedPatternDiscoverer = new QuarantinedPatternDiscoverer(); 38 | Class delegateRunningTo = delegateRunningToDiscoverer.getDelegateRunningToOn(testFileClass); 39 | 40 | String testFileClassName = testFileClass.getName(); 41 | String delegateRunningToClassName = delegateRunningTo.getName(); 42 | 43 | String[] quarantinedPatterns = quarantinedPatternDiscoverer.getQuarantinedPatternsOn(testFileClass); 44 | String[] allPatterns = Arrays.copyOf(quarantinedPatterns, quarantinedPatterns.length + 2); 45 | allPatterns[quarantinedPatterns.length] = testFileClassName; 46 | allPatterns[quarantinedPatterns.length + 1] = delegateRunningToClassName; 47 | 48 | QuarantiningUrlClassLoader classLoader = new QuarantiningUrlClassLoader(allPatterns); 49 | 50 | try 51 | { 52 | innerRunnerClass = classLoader.loadClass(delegateRunningToClassName); 53 | Class testClass = classLoader.loadClass(testFileClassName); 54 | innerRunner = innerRunnerClass.cast(innerRunnerClass.getConstructor(Class.class).newInstance(testClass)); 55 | } 56 | catch (InstantiationException | IllegalAccessException | IllegalArgumentException | InvocationTargetException 57 | | NoSuchMethodException | SecurityException | ClassNotFoundException e) 58 | { 59 | throw new InitializationError(e); 60 | } 61 | } 62 | 63 | 64 | @Override 65 | public Description getDescription() 66 | { 67 | try 68 | { 69 | return (Description) innerRunnerClass.getMethod("getDescription").invoke(innerRunner); 70 | } 71 | catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException 72 | | SecurityException e) 73 | { 74 | throw new RuntimeException("Could not get description", e); 75 | } 76 | } 77 | 78 | @Override 79 | public void run(RunNotifier notifier) 80 | { 81 | try 82 | { 83 | innerRunnerClass.getMethod("run", RunNotifier.class).invoke(innerRunner, notifier); 84 | } 85 | catch (IllegalAccessException | IllegalArgumentException | InvocationTargetException | NoSuchMethodException 86 | | SecurityException e) 87 | { 88 | notifier.fireTestFailure(new Failure(getDescription(), e)); 89 | } 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 3 | 4.0.0 4 | com.binarytweed 5 | quarantining-test-runner 6 | 0.0.3 7 | 8 | jar 9 | ${project.groupId}:${project.artifactId} 10 | JUnit Runner that loads specified classes in a separate ClassLoader 11 | http://https://github.com/BinaryTweed/quarantining-test-runner 12 | 13 | 14 | MIT License 15 | http://www.opensource.org/licenses/mit-license.php 16 | 17 | 18 | 19 | 20 | Deejay 21 | github@binarytweed.com 22 | Binary Tweed 23 | http://www.binarytweed.com 24 | 25 | 26 | 27 | scm:git:git@github.com:binarytweed/quarantining-test-runner.git 28 | scm:git:git@github.com:binarytweed/quarantining-test-runner.git 29 | git@github.com:binarytweed/quarantining-test-runner.git 30 | 31 | 32 | 33 | 34 | ossrh 35 | https://oss.sonatype.org/content/repositories/snapshots 36 | 37 | 38 | 39 | 40 | 41 | org.apache.maven.plugins 42 | maven-compiler-plugin 43 | 3.2 44 | 45 | 1.7 46 | 1.7 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | release-profile 55 | 56 | 57 | gpg.passphrase 58 | 59 | 60 | 61 | 62 | 63 | org.apache.maven.plugins 64 | maven-source-plugin 65 | 66 | 67 | attach-sources 68 | 69 | jar 70 | 71 | 72 | 73 | 74 | 75 | org.apache.maven.plugins 76 | maven-javadoc-plugin 77 | 78 | 79 | attach-javadocs 80 | 81 | jar 82 | 83 | 84 | 85 | 86 | 87 | maven-scm-plugin 88 | 1.9.2 89 | 90 | ${project.version} 91 | 92 | 93 | 94 | org.apache.maven.plugins 95 | maven-gpg-plugin 96 | 97 | 98 | sign-artifacts 99 | verify 100 | 101 | sign 102 | 103 | 104 | 105 | 106 | ${project.basedir}/pubring.gpg 107 | ${project.basedir}/secring.gpg 108 | 109 | 110 | 111 | org.sonatype.plugins 112 | nexus-staging-maven-plugin 113 | 1.6.3 114 | true 115 | 116 | ossrh 117 | https://oss.sonatype.org/ 118 | true 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | junit 129 | junit 130 | 4.11 131 | 132 | 133 | 134 | org.slf4j 135 | slf4j-api 136 | 1.7.10 137 | 138 | 139 | 140 | ch.qos.logback 141 | logback-classic 142 | 1.1.2 143 | test 144 | 145 | 146 | 147 | org.hamcrest 148 | hamcrest-core 149 | 1.3 150 | test 151 | 152 | 153 | 154 | org.hamcrest 155 | hamcrest-library 156 | 1.3 157 | test 158 | 159 | 160 | --------------------------------------------------------------------------------