Let me show you an example. Assume we have a MyService class:
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class MyService | |
{ | |
private static MyService instance = new MyService(); | |
private String someConfig; | |
private MyService() { } | |
static public MyService getInstance() { | |
return instance; | |
} | |
public void init(String config) { | |
if (someConfig == null) | |
someConfig = config; | |
else | |
throw new IllegalStateException("MyService has already been initialized."); | |
} | |
public void doSomeService(FileWriter w) throws IOException { | |
w.write(someConfig); | |
} | |
} |
- init(String config)
- to initialize MyService with a config parameter. This method can only call once.
- doSomeService(FileWriter)
- ask MyService to do something. In this example, it writes the value of someConfig via FileWriter parameter.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RunWith(MockitoJUnitRunner.class) | |
public class MyService_SayHello_Test { | |
@Mock FileWriter fw; | |
@Test | |
public void doSomeService_SayHello() throws Exception { | |
MyService service = MyService.getInstance(); | |
service.init("Hello!"); | |
service.doSomeService(fw); | |
verify(fw).write("Hello!"); | |
} | |
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
@RunWith(MockitoJUnitRunner.class) | |
public class MyService_SayHi_Test { | |
@Mock FileWriter fw; | |
@Test | |
public void doSomeService_SayHello() throws Exception { | |
MyService service = MyService.getInstance(); | |
service.init("Hi!"); | |
service.doSomeService(fw); | |
verify(fw).write("Hi!"); | |
} | |
} |
As mentioned at the beginning, JUnit tests are run in one instance of JVM. If you run both test cases, because of singleton design pattern, the MyService instance of first test case is carried over to the second test case. When the second test case calls the init(...) method, it throws exception. This is actually one of the infamous disadvantages of singleton design pattern. However, you may not be able to re-design your system. Then, how can we fix the unit tests?
Actually, there is a workaround to force reloading of classes for each test using class loader trick. For each JUnit test class, we use a customized test runner which enforce a separated instance of class loader for each test class. Then, the classes we want to test will be reloaded for each JUnit test class Here is the implementation of the test runner (SeparateClassloaderTestRunner):
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
public class SeparateClassloaderTestRunner extends MockitoJUnitRunner { | |
public SeparateClassloaderTestRunner(Class<?> clazz) throws InitializationError, InvocationTargetException { | |
super(getFromTestClassloader(clazz)); | |
} | |
private static Class<?> getFromTestClassloader(Class<?> clazz) throws InitializationError { | |
try { | |
ClassLoader testClassLoader = new TestClassLoader(); | |
return Class.forName(clazz.getName(), true, testClassLoader); | |
} catch (ClassNotFoundException e) { | |
throw new InitializationError(e); | |
} | |
} | |
public static class TestClassLoader extends URLClassLoader { | |
public TestClassLoader() { | |
super(((URLClassLoader)getSystemClassLoader()).getURLs()); | |
} | |
@Override | |
public Class<?> loadClass(String name) throws ClassNotFoundException { | |
if (name.startsWith("loklam.")) { //TODO update the correct package to fit your case. | |
return super.findClass(name); | |
} | |
return super.loadClass(name); | |
} | |
} | |
} |
You can get the demo source code at github - https://github.com/loklam/reloadclassdemo.