Reply to comment
"Untestable" code - JMockit
Submitted by Tomek Kaczanowski on Sun, 04/20/2008 - 20:13This is a part of "Untestable code" series. See the introduction to know what is it all about (yes, you really should go there, do it).
The main idea of the series is to write unit-tests for a particularly nasty piece of code. In this part I will use JMockit library to write test cases.
Please notice the source code attached at the end of this text.
It's worth mentioning here, that this "particularly nasty piece of code" which is the base of all articles from the "untestable code" series is taken from JMockit tutorial. ...what's even more important, the test classes are taken from JMockit tutorial as well, which means I am NOT an author of these classes - they were created by Rogério Liesenfeld.
JMockit
JMockit is a bunch of test-related projects created by Rogério Liesenfeld. I don't aim at evaluating all of them - I only take what's needed to solve the problem. For more info about JMockit see the project homepage.
Original class
This is the class under test:
public final class ServiceA {
public void doBusinessOperationXyz(EntityX data)
throws InvalidItemStatus {
List items = Database.find(
"select item from EntityY item where item.someProperty=?",
data.getSomeProperty());
BigDecimal total = new ServiceB().computeTotal(items);
data.setTotal(total);
Database.save(data);
}
}
Changing the original class
There is no need to change the original class. Even final keyword can stay as it is - JMockit doesn't need to create subclasses of mocked classes, so it doesn't care about final.
Test class
public class ServiceATest extends TestCase
{
private boolean serviceMethodCalled;
public static class MockDatabase
{
static int findMethodCallCount;
static int saveMethodCallCount;
public static void save(Object o)
{
assertNotNull(o);
saveMethodCallCount++;
}
public static List find(String ql, Object arg1)
{
assertNotNull(ql);
assertNotNull(arg1);
findMethodCallCount++;
return Collections.EMPTY_LIST;
}
}
protected void setUp() throws Exception
{
super.setUp();
MockDatabase.findMethodCallCount = 0;
MockDatabase.saveMethodCallCount = 0;
Mockit.redefineMethods(Database.class, MockDatabase.class);
}
public void testDoBusinessOperationXyz() throws Exception
{
final BigDecimal total = new BigDecimal("125.40");
Mockit.redefineMethods(ServiceB.class, new Object() {
public BigDecimal computeTotal(List items)
{
assertNotNull(items);
serviceMethodCalled = true;
return total;
}
});
EntityX data = new EntityX(5, "abc", "5453-1");
new ServiceA().doBusinessOperationXyz(data);
assertEquals(total, data.getTotal());
assertTrue(serviceMethodCalled);
assertEquals(1, MockDatabase.findMethodCallCount);
assertEquals(1, MockDatabase.saveMethodCallCount);
}
public void testDoBusinessOperationXyzWithInvalidItemStatus()
{
Mockit.redefineMethods(ServiceB.class, new Object() {
public BigDecimal computeTotal(List items) throws InvalidItemStatus
{
throw new InvalidItemStatus();
}
});
EntityX data = new EntityX(5, "abc", "5453-1");
try {
new ServiceA().doBusinessOperationXyz(data);
fail(InvalidItemStatus.class + " was expected");
}
catch (InvalidItemStatus ignore) {
// ok, test passed
assertNull(data.getTotal());
assertEquals(1, MockDatabase.findMethodCallCount);
assertEquals(0, MockDatabase.saveMethodCallCount);
}
}
}
This one looks rather odd for people who (like me) are used to use some mock framework to do the dirty work for them. MockDatabase is a test-spy written by hand, which I find evil or at least cumbersome - why write something that can be written automatically ? Believe me or not, but it's not laziness which makes me put this question. I am deeply convinced that unit tests should be as easy as they can be, therefore I try not to put any logic - even the simplest one - into it.
Same goes for redefined methods computeTotal.
The magic resides in lines like these:
Mockit.redefineMethods(Database.class, MockDatabase.class);
Mockit.redefineMethods(ServiceB.class, ...);
What does it mean ? Exactly what it says - "don't use real Database, use MockDatabase instead", "don't use original computeTotal method of ServiceB class, use the one that I give you here", and so on.
How does it work ?
Behind the scene, JMockit depends on the JVM class redefinition mechanism. It's really amazing ! - see the sources of JMockit-core (yes, go and see for yourself right now), there are only few classes there, but the whole thing is very powerful.
Pros and Cons
Pros
- There is no need to change class under test - even
finalkeyword is not a problem (which is unique feature of JMockit - I haven't seen any other mock-framework capable of this). - Code coverage with cobertura works fine. (It's good to know, that JMockit comes with it own coverage tool - see JMockit Coverage for details - I haven't tried it out yet).
Cons
- Need to write mocks (test-spies) by hand, which means two things:
- test class is big
- some logic must be introduced into the test class
My 3 cents
I'm impressed by the JMockit's ability to mock final classes. But in fact, I don't recall that I missed this feature very often. What I don't like is that you have to write mocks by hand - I think it's a step back from the path drawn by jmock, easymock and others.
JMockit artifacts
As for now (21/04/2008, last JMockit version is 0.94c) there are no JMockit jars available on mvnrepository.com, so you need to download it from the project site and install in your local repo by hand.
Attachmets
The file with maven project is attached at the end of this text. Unzip and run with mvn test site. Results of tests will be printed directly to the console. Code coverage report is available in target/site directory (see index.html in there).
Links
All links gathered here.
| Attachment | Size |
|---|---|
| uc-jmockit.zip | 10.93 KB |