"Untestable" code - JEasyTest
This 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 JEasyTest library to write test cases.
Please notice the source code attached at the end of this text.
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. But we need to drop "final" keyword from ServiceB
cause we use normal mocks to fake it.
Test class
@ClassUnderTest(ServiceA.class)
public class ServiceATest extends TestCase {
private EntityX entity;
private ServiceA serviceA;
private ServiceB servB;
private final BigDecimal total = new BigDecimal("125.40");
@Override
protected void setUp() throws Exception {
entity = new EntityX(1, "entityName", "entityCode");
serviceA = new ServiceA();
servB = createMock(ServiceB.class);
}
@JEasyTest
public void testBusinessOperation() throws InvalidItemStatus {
on(Database.class).expectStaticNonVoidMethod("find").with(
arg("select item from EntityY item where item.someProperty=?"),
arg("abc")).andReturn(Collections.EMPTY_LIST);
on(ServiceB.class).expectEmptyConstructor().andReturn(servB);
on(Database.class).expectStaticVoidMethod("save").with(arg(entity));
expect(servB.computeTotal(Collections.EMPTY_LIST)).andReturn(total);
replay(servB);
serviceA.doBusinessOperationXyz(entity);
verify(servB);
assertEquals(total, entity.getTotal());
}
}
Things to notice here:
@ClassUnderTest(ServiceA.class)
annotation tells JEasyTest, well, it's kinda obvious - checktarget/jeasytest/generatedAspects
directory to see what it does@JEasyTest
annotation tells JEasyTest library to take care of this test-method
The real magic resides in lines like this one:
on(ServiceB.class).expectEmptyConstructor().andReturn(servB);
You can simply read it:
"When empty constructor of ServiceB
class is invoked, return servB
object (instead of really executing constructor and creating the new object of ServiceB
class).
Other static methods seen in the test class (expect(), replay()
and verify()
) are specific to EasyMock library, and have nothing to do with JEasyTest.
How does it work ?
Behind the scene, JEasyTest creates aspects, weaves them into original code, and pass this woven classes to jUnit.
Maven plugin takes care of all these, so you won't even noticed.
Pros and Cons
Pros
- There is no need to change class under test - source code stays the way it is (well, almost - see Cons below).
- Test class is very easy to understand (especially for people familiar with any mock library).
Cons
- Won't work if
ServiceB
isfinal
as we can't mock final classes. - Simple refactoring can damage your test (try to rename
Database.find
method). - Code coverage is a problem (I don't know how to measure code coverage for this aspect-based solution, but maybe there is a way - let me know please).
- Generation of aspects and weaving takes some additional time during execution of tests (which might hurt if you fire tests often).
My 3 cents
I love the way the test class is written - it's very easy to understand what's going on there. And there is no need to alter the source code.
If I only knew how to measure code coverage...
Attachments
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 useless - it shows 0% coverage. :(
Links
All links gathered here.
Attachment | Size |
---|---|
uc-jeasytest.zip | 11.4 KB |
This used to be my blog. I moved to http://tomek.kaczanowscy.pl long time ago.