Question
Is it possible to provide an implementation of a class, using a custom ClassLoader
, that will be properly utilized from a static context?
Background
I am working with a framework which recommends we use a static class to wire up dependencies.
It works something like this..
public class MyClass {
@ThisIsADependency
private MyDependency myDependency;
public void initialize() {
FrameworkProvidedDependencyResolver.resolveDependencies(this);
}
}
As you might expect, this is a nightmare to test with, and, sure enough FrameworkProvidedDependencyResolver
(not the real name) throws a NullPointerException
unless called from within an active framework environment which is not possible from JUnit.
What I would like to do, is provide a custom ClassLoader
which I can use in JUnit tests to provide a custom FrameworkProvidedDependencyResolver
which wires up mock dependencies or whatever.
Ok, so here's what I'd like my unit tests to look like:
@RunWith(MyTestRunner.class)
public class TestMyClass {
@Test
public void testInitialization() {
MyClass myClass = new MyClass();
myClass.initialize();
// not much of a test, I know
}
}
MyTestRunner
is where I opt to use my custom ClassLoader
..
public class MyTestRunner extends BlockJUnit4ClassRunner {
public MyTestRunner(Class<?> clazz) throws InitializationError {
super(getFromMyClassLoader(clazz));
}
private static Class<?> getFromMyClassLoader(Class<?> clazz) throws InitializationError {
try {
ClassLoader testClassLoader = new MyClassLoader();
return Class.forName(clazz.getName(), true, testClassLoader);
} catch (ClassNotFoundException e) {
throw new InitializationError(e);
}
}
}
Thanks @AutomatedMike.
Ok, so that slips MyClassLoader
into the mix where I can get a chance to swap out FrameworkProvidedDependencyResolver
with a custom dependency resolver for testing..
public class ZKTestClassLoader extends URLClassLoader {
public ZKTestClassLoader() {
super(((URLClassLoader) getSystemClassLoader()).getURLs());
}
@Override
public Class<?> loadClass(String name) throws ClassNotFoundException {
Class<?> loadedClass = findLoadedClass(name);
if (loadedClass != null) {
return loadedClass;
}
System.out.println("Loading " + name);
if (name.startsWith("my.test.classes")) {
// Make sure we use MyClassLoader to load the test classes,
// thus any classes it loads (eg: MyClass) will come through here.
return super.findClass(name);
} else if (name.endsWith("FrameworkProvidedDependencyResolver")) {
// What should do we do here?
}
return super.loadClass(name);
}
}
Ok, and so now we are in a position to load a custom FrameworkProvidedDependencyResolver
instead of the one provided by the framework.. but how do I do that?
I can ignore the request for 'FrameworkProvidedDependencyResolver' and return another class, say, 'MyMockFrameworkProvidedDependencyResolver'. That's fine but when MyClass.initialize
calls for FrameworkProvidedDependencyResolver
from a static context, we get a NoClassDefFoundError
. Makes sense.
I can try naming MyMockFrameworkProvidedDependencyResolver
the same as the real FrameworkProvidedDependencyResolver
and put it in another package (eg: i.hate.my.framework.FrameworkProvidedDependencyResolver
). This also doesn't work as the MyClass
is specifically looking at the real FrameworkProvidedDependencyResolver
, package and all.
I can try naming my class as the real FrameworkProvidedDependencyResolver
and putting it in the same package as that provided by my framework.. but now I don't even need a ClassLoader
. the JVM will be confused by the two and load whichever is appropriate by the classpath, likely mine. The problem here is that this now applies to all tests; not the solution I'm looking for.
Lastly, I cannot use Proxy
because FrameworkProvidedDependencyResolver
is not an interface
.
Ok, to restate my question:
Is it possible to provide an implementation of a class, using a custom ClassLoader
, that will be properly utilized from a static context? Perhaps, can I have a class in it's own unique path with a unique name, which I can edit as I load it, such that it will appear in the JVM with the expected path and name I'm trying to override? Any other solution is, of course, welcome.