Skip to content

Commit

Permalink
Bug: ParentRunner lost test Class from a separate class loader (junit…
Browse files Browse the repository at this point in the history
…-team#1252)

* Bug: ParentRunner lost test Class from a separate class loader

When junit.jar located in one ClassLoader but runing tests in another ParentRunner can lost information about run class.
For example if use @ClassRule and request test class(org.junit.runner.Description#getTestClass) we can get null,
because ParentRunner instead of set Class as is to Description tranform it to class name string,
as result org.junit.runner.Description#getTestClass execute Class.forName and can't find test class.

Spring-test fail with exception if we try use SpringClassRule

```
java.lang.NullPointerException
 at org.springframework.test.context.junit4.rules.SpringClassRule.validateSpringMethodRuleConfiguration(SpringClassRule.java:186)
 at org.springframework.test.context.junit4.rules.SpringClassRule.apply(SpringClassRule.java:134)
 at org.junit.rules.RunRules.applyAll(RunRules.java:26)
 at org.junit.rules.RunRules.<init>(RunRules.java:15)
 at org.junit.runners.ParentRunner.withClassRules(ParentRunner.java:245)
 at org.junit.runners.ParentRunner.classBlock(ParentRunner.java:194)
 at org.junit.runners.ParentRunner.run(ParentRunner.java:362)
 at org.junit.runner.JUnitCore.run(JUnitCore.java:137)
```

As solution, now ParentRunner create Descriptor with explicit specify Class, name and uses annotations.

* Restore backward compatibility with sub class of ParentRunner that override getName method
* Add ParentRunnerClassLoaderTest to AllClassesTests suite
  • Loading branch information
Gordiychuk authored and kcooney committed Sep 17, 2016
1 parent 8295c93 commit 323353b
Show file tree
Hide file tree
Showing 5 changed files with 161 additions and 2 deletions.
11 changes: 11 additions & 0 deletions src/main/java/org/junit/runner/Description.java
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,17 @@ public static Description createSuiteDescription(Class<?> testClass) {
return new Description(testClass, testClass.getName(), testClass.getAnnotations());
}

/**
* Create a <code>Description</code> named after <code>testClass</code>
*
* @param testClass A not null {@link Class} containing tests
* @param annotations meta-data about the test, for downstream interpreters
* @return a <code>Description</code> of <code>testClass</code>
*/
public static Description createSuiteDescription(Class<?> testClass, Annotation... annotations) {
return new Description(testClass, testClass.getName(), annotations);
}

/**
* Describes a Runner which runs no tests
*/
Expand Down
12 changes: 10 additions & 2 deletions src/main/java/org/junit/runners/ParentRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -347,8 +347,16 @@ protected Annotation[] getRunnerAnnotations() {

@Override
public Description getDescription() {
Description description = Description.createSuiteDescription(getName(),
getRunnerAnnotations());
Class<?> clazz = getTestClass().getJavaClass();
Description description;
// if subclass overrides `getName()` then we should use it
// to maintain backwards compatibility with JUnit 4.12
if (clazz == null || !clazz.getName().equals(getName())) {
description = Description.createSuiteDescription(getName(), getRunnerAnnotations());
} else {
description = Description.createSuiteDescription(clazz, getRunnerAnnotations());
}

for (T child : getFilteredChildren()) {
description.addChild(describeChild(child));
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.junit.runner.RunWith;
import org.junit.runners.Suite;
import org.junit.runners.Suite.SuiteClasses;
import org.junit.tests.running.classes.parent.ParentRunnerClassLoaderTest;

@RunWith(Suite.class)
@SuiteClasses({
Expand All @@ -13,6 +14,7 @@
ParameterizedTestTest.class,
ParentRunnerFilteringTest.class,
ParentRunnerTest.class,
ParentRunnerClassLoaderTest.class,
RunWithTest.class,
SuiteTest.class,
UseSuiteAsASuperclassTest.class
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package org.junit.tests.running.classes.parent;


import org.junit.Test;
import org.junit.runner.Description;
import org.junit.runner.notification.RunNotifier;
import org.junit.runners.BlockJUnit4ClassRunner;
import org.junit.runners.ParentRunner;
import org.junit.runners.model.FrameworkMethod;
import org.junit.runners.model.InitializationError;

import java.lang.reflect.Field;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.List;

import static org.junit.Assert.assertEquals;

public class ParentRunnerClassLoaderTest {
@Test
public void testClassRuleAccessToClassInAnotherClassLoader() throws Exception {
Class<?> testClassWithOwnClassLoader = wrapToClassLoader(TestWithClassRule.class);

runTestWithParentRunner(testClassWithOwnClassLoader);

Field fieldWithReference = testClassWithOwnClassLoader.getDeclaredField("applyTestClass");
Class<?> usedClass = (Class<?>) fieldWithReference.get(null);

assertEquals("JUnitRunner can be located in own classLoader, so, " +
"Class.forName org.junit.runner.Description.getTestClass can not see " +
"in current classloader by execute Class.forName",
testClassWithOwnClassLoader, usedClass
);
}

@Test
public void testDescriptionContainCorrectTestClass() throws Exception {
Class<?> testClassWithOwnClassLoader = wrapToClassLoader(TestWithClassRule.class);
ParentRunner<?> runner = new BlockJUnit4ClassRunner(testClassWithOwnClassLoader);

Description description = runner.getDescription();
assertEquals("ParentRunner accept already instantiate Class<?> with tests, if we lost it instance, and will " +
"use Class.forName we can not find test class again, because tests can be " +
"located in different ClassLoader",
description.getTestClass(), testClassWithOwnClassLoader
);
}

@Test
public void testBackwardCompatibilityWithOverrideGetName() throws Exception {
final Class<TestWithClassRule> originalTestClass = TestWithClassRule.class;
final Class<?> waitClass = ParentRunnerClassLoaderTest.class;

ParentRunner<FrameworkMethod> subParentRunner = new BlockJUnit4ClassRunner(originalTestClass) {
@Override
protected String getName() {
return waitClass.getName();
}
};

Description description = subParentRunner.getDescription();
Class<?> result = description.getTestClass();

assertEquals("Subclass of ParentRunner can override getName method and specify another test class for run, " +
"we should maintain backwards compatibility with JUnit 4.12",
waitClass, result
);
}

private void runTestWithParentRunner(Class<?> testClass) throws InitializationError {
ParentRunner<?> runner = new BlockJUnit4ClassRunner(testClass);
runner.run(new RunNotifier());
}

private Class<?> wrapToClassLoader(Class<?> sourceClass) throws ClassNotFoundException {
URL classpath = sourceClass.getProtectionDomain().getCodeSource().getLocation();
VisibleClassLoader loader = new VisibleClassLoader(new URL[]{classpath}, this.getClass().getClassLoader());
Class<?> testClassWithOwnClassLoader = loader.findClass(sourceClass.getName());

assert testClassWithOwnClassLoader != sourceClass;

return testClassWithOwnClassLoader;
}


private static class VisibleClassLoader extends URLClassLoader {
public VisibleClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}

@Override // just making public
public Class<?> findClass(String name) throws ClassNotFoundException {
return super.findClass(name);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package org.junit.tests.running.classes.parent;

import org.junit.Assert;
import org.junit.ClassRule;
import org.junit.Test;
import org.junit.rules.TestRule;
import org.junit.runner.Description;
import org.junit.runners.model.Statement;

import java.lang.reflect.Field;

/**
* Test class for validate run tests that was load in own ClassLoader
*/
public class TestWithClassRule {
public static Class<?> applyTestClass;

@ClassRule
public static TestRule rule = new CustomRule();

@Test
public void testClassRuleExecuted() throws Exception {
Assert.assertNotNull("Description should contain reference to TestClass", applyTestClass);
}

public static final class CustomRule implements TestRule {

public Statement apply(final Statement base, final Description description) {
return new Statement() {
@Override
public void evaluate() throws Throwable {
Class<?> testClass = description.getTestClass();
if(testClass != null) {
Field field = testClass.getDeclaredField("applyTestClass");
field.set(null, description.getTestClass());
}
base.evaluate();
}
};
}
}
}

0 comments on commit 323353b

Please sign in to comment.