Using EasyMock class extensions in Eclipse PDE tests
June 3, 2013 | 4 min ReadEasyMock is a powerful framework to create mock objects to use in Java JUnit tests. When used in an OSGi environment (e.g. to write Eclipse PDE tests), the creation of class extensions can cause trouble. This post describes a problematic use case and possible solutions.
The use case
Imagine the following situation: an Eclipse RCP application has a bundle A
containing a class MyClass
. This class has a field of type UsedClass
which resides in another bundle B
. Obviously A
needs to have dependency to B
. When writing (PDE) tests for MyClass
, it is good practice to put the test class (let’s call it MyClass_PDETest
) into a fragment of A
- let’s name it A.test
. In our test, we would like to mock UsedClass
via EasyMock class extension and inject it into our test instance of MyClass
:
@Test
public void testMyClassWithMock() throws Exception {
MyClass uut = new MyClass();
UsedClass mock = EasyMock.createMock(UsedClass.class);
uut.setUsedClass(mock);
... // implement assertions here
}
Since our test uses EasyMock, the test fragment A.test
has dependencies to the EasyMock bundle and to the CGLib and Objenesis bundles (which are needed for the creation of the mock):
When we run our test, mock creation fails with an Exception:
net.sf.cglib.core.CodeGenerationException: java.lang.reflect.InvocationTargetException–>null
Further down the stack we find:
Caused by: java.lang.ClassNotFoundException: mypackage.UsedClass
The problem
To understand what happens here, we need to have a look at the way how EasyMock creates class extensions. EasyMock delegates the mock creation to CGLib’s Enhancer
class which tries to create a dynamic subclass of the class to mock. To do so, it needs a class loader which can load the class to mock. EasyMock first tries the class loader from the class to mock, in our case the class loader of bundle B
which contains UsedClass
. This fails because due to the strict class loading policy of OSGi, B
does not know CGLib. Then it tries to use the class loader of EasyMock which fails as well since the EasyMock bundle does not have a dependency to B
, resulting in the ClassNotFoundException
experienced above.
Note that this is only a problem when mocking an object which is not located in the host bundle of the test fragment (otherwise CGLib would be known). Additionally it only occurs when mocking classes, not interfaces.
Solutions
This problem has already been reported to EasyMock some years ago. There is a proposed solution in the comments to this bug report, but it has not found its way into the code base up to today (EasyMock 3.1). Here are a number of choices to solve the problem:
- Apply the patch from the comments to the EasyMock bundle in your project - it essentially combines the two class loaders above into one class loader which has access to both CGLib and the class to mock. Apparently this has the drawback that patching of 3rd party libraries always has: you have to apply your changes every time you upgrade to a newer version.
- Give EasyMock’s class loader access to the class to mock by using dynamic import packages or buddy policies. E.g. for dynamic import packages, add the following line to the
MANIFEST.MF
of the EasyMock bundle:
DynamicImport-Package: \*
You still need to patch EasyMock with this solution but the change is smaller.
- Create a test fragment
B.test
which has a dependency to CGLib. When the test is executed, the class loader ofB
can then be used to instantiate the mock: The test fragment does not have to contain any tests itself, it only needs to be present when the PDE tests are executed. This solution does not need changes in 3rd party libraries, but it requires to create test fragments for each bundle containing classes to mock.
We decided to go for solution number 3. There is no need to patch 3rd party code, and since we strive to develop test-driven, there are test fragments for almost all our bundles anyway. It only feels weird if you try to mock classes from 3rd party libraries - in this case the test fragment will never contain any tests. But fortunately the use of interfaces is common practice nowadays, so this is really rarely the case.