I do TDD, but I do not start writing unit tests
before writing production code. It's not that I didn't try - I gave it an honest attempt for quite some time, before I gave up. Somehow the approach of writing tests
first does not seem intuitive to me. At least I need to figure out the collaborators and the initial subset of public contracts before my TestNG plugin kicks in. Since then, it's all a game of collaborative pingpong between my production code and test code. Having said that, I honestly believe that an exhaustive unit test suite is no less important than the code itself. I take every pain to ensure that my unit tests are well organized, properly refactored and follow all principles of good programming that I espouse while writing the actual production code.
Refactoring is one practice that I preach, teach and encourage vigorously to my teammates. Same for unit tests - if I strive to make my production code speak the language of the domain, so should be my unit tests. This post is about a similar refactoring exercise to make unit tests speak the DSL. At the end of this effort of vigorous iterations and cycles of refactoring, we could achieve a well engineered unit test suite, which can probably be enhanced by the domain guys with minimum of programming knowledge.
The Class Under TestThe following is the fragment of a simplified version of an
AccruedInterestCalculator
, a domain class, which calculates the interest accrued for a client over a period of time. For brevity, I have a very simplified version of the class with only very simple domain logic. The idea is to develop a fluent unit test suite for this class that speaks the domain language through an iterative process of refactoring. I will be using the artillery consisting of the state of the art unit testing framework (
TestNG), a dynamic mocking framework (
EasyMock) and the most powerful weapon in programming - merciless refactoring.
public class AccruedInterestCalculator {
//..
//..
private IAccruedDaysCalculator accruedDaysCalculator;
private IInterestRateCalculator interestRateCalculator;
public final BigDecimal calculateAccruedInterest(final BigDecimal principal)
throws NoInterestAccruedException {
int days = accruedDaysCalculator.calculateAccruedDays();
if (days == 0) {
throw new NoInterestAccruedException("Zero accrual days for principal " + principal);
}
BigDecimal rate = interestRateCalculator.calculateRate();
BigDecimal years = BigDecimal.valueOf(days).divide(BigDecimal.valueOf(365), 2, RoundingMode.UP);
return principal.multiply(years).multiply(rate);
}
AccruedInterestCalculator setAccruedDaysCalculator(IAccruedDaysCalculator accruedDaysCalculator) {
this.accruedDaysCalculator = accruedDaysCalculator;
return this;
}
AccruedInterestCalculator setInterestRateCalculator(IInterestRateCalculator interestRateCalculator) {
this.interestRateCalculator = interestRateCalculator;
return this;
}
}
and the two collaborators ..
public interface IAccruedDaysCalculator {
int calculateAccruedDays();
}
public interface IInterestRateCalculator {
BigDecimal calculateRate();
}
Mocks are useful, but Noisy ..I am a big fan of using mocks for unit testing -
EasyMock is my choice of dynamic mocking framework. Mocks provide the most seamless way of handling collaborators while writing unit tests for a class. However, often, I find mocks introducing lots of boilerplate codes which need to be repeated for every test method that I write ..
public class AccruedInterestCalculatorTest {
protected IMocksControl mockControl;
@BeforeMethod
public final void setup() {
mockControl = EasyMock.createControl();
}
@Test
public void normalAccruedInterestCalculation() {
IAccruedDaysCalculator acalc = mockControl.createMock(IAccruedDaysCalculator.class);
IInterestRateCalculator icalc = mockControl.createMock(IInterestRateCalculator.class);
expect(acalc.calculateAccruedDays()).andReturn(2000);
expect(icalc.calculateRate()).andReturn(BigDecimal.valueOf(0.5));
mockControl.replay();
BigDecimal interest =
new AccruedInterestCalculator()
.setAccruedDaysCalculator(acalc)
.setInterestRateCalculator(icalc)
.calculateAccruedInterest(BigDecimal.valueOf(2000.00));
mockControl.verify();
}
}
Refactoring! Refactoring!Have a look at the above test method - the mock framework setup and controls pollute the actual business logic that I would like to test. Surely, not a very domain friendly approach. As Howard has pointed to, in one of his NFJS
writings, we can abstract away the mock stuff into separate methods within the test class, or still better in a separate
MockControl
class altogether. This makes the actual test class lighter and free of some of the noise. Here is the snapshot after one round of refactoring ..
// mock controls delegated to class MockControl
public class AccruedInterestCalculatorTest extends MockControl {
@BeforeMethod
public final void setup() {
mockControl = EasyMock.createControl();
}
@Test
public void normalAccruedInterestCalculation() {
IAccruedDaysCalculator acalc = newMock(IAccruedDaysCalculator.class);
IInterestRateCalculator icalc = newMock(IInterestRateCalculator.class);
expect(acalc.calculateAccruedDays()).andReturn(2000);
expect(icalc.calculateRate()).andReturn(BigDecimal.valueOf(0.5));
replay();
BigDecimal interest =
new AccruedInterestCalculator()
.setAccruedDaysCalculator(acalc)
.setInterestRateCalculator(icalc)
.calculateAccruedInterest(BigDecimal.valueOf(2000.00));
verify();
}
}
and the new
MockControl
class ..
public abstract class MockControl {
protected IMocksControl mockControl;
protected final <T> T newMock(Class<T> mockClass) {
return mockControl.createMock(mockClass);
}
protected final void replay() {
mockControl.replay();
}
protected final void verify() {
mockControl.verify();
}
}
The test class
AccruedInterestCalculatorTest
is now cleaner and the test method is lighter in baggage from the guts of the mock calls. But still it is not sufficiently close to speaking the domain language. One of the litmus tests which we often do to check the domain friendliness of unit tests is to ask a domain guy to explain the unit test methods (annotated with
@Test
) and, if possible, to enhance them. This will definitely be a smell to them, with the remaining litterings of mock creation and training still around. The domain guy in this case, nods a big NO, Eclipse kicks in, and we start the next level of refactoring.
One thing strikes me - each test method is a composition of the following steps :
- create mocks
- setup mocks
- replay
- do the actual stuff
- verify
And refactoring provides me the ideal way to scaffold all of these behind fluent interfaces for the contract that we plan to test. How about a scaffolding class that encapsulates these steps ? I keep the scaffold as a private inner class and try to localize all the mockeries in one place. And the scaffold can always expose fluent interfaces to be used by the test methods. Here is what we have after a couple of more rounds of iterative refactoring ..
public class AccruedInterestCalculatorTest extends MockControl {
private IAccruedDaysCalculator acalc;
private IInterestRateCalculator icalc;
@BeforeMethod
public final void setup() {
mockControl = EasyMock.createControl();
acalc = newMock(IAccruedDaysCalculator.class);
icalc = newMock(IInterestRateCalculator.class);
}
// the scaffold class encapsulating all mock methods
private class CalculatorScaffold {
private BigDecimal principal;
private int days;
private BigDecimal rate;
private BigDecimal interest;
CalculatorScaffold on(BigDecimal principal) {
this.principal = principal;
return this;
}
CalculatorScaffold forDays(int days) {
this.days = days;
return this;
}
CalculatorScaffold at(BigDecimal rate) {
this.rate = rate;
return this;
}
CalculatorScaffold calculate() {
expect(acalc.calculateAccruedDays()).andReturn(days);
expect(icalc.calculateRate()).andReturn(rate);
replay();
interest =
new AccruedInterestCalculator()
.setAccruedDaysCalculator(acalc)
.setInterestRateCalculator(icalc)
.calculateAccruedInterest(principal);
verify();
return this;
}
void andCheck(BigDecimal interestGold) {
assert interest.compareTo(interestGold) == 0;
}
}
//..
//.. the actual test methods
@Test
public void normalAccruedInterest() {
new CalculatorScaffold()
.on(BigDecimal.valueOf(1000.00))
.forDays(days)
.at(BigDecimal.valueOf(0.5))
.calculate()
.andCheck(BigDecimal.valueOf(30.0));
}
@Test
public void normalAccruedInterest() {
new CalculatorScaffold()
.on(BigDecimal.valueOf(2000.00))
.forDays(days)
.at(BigDecimal.valueOf(0.5))
.calculate()
.andCheck(BigDecimal.valueOf(60.0));
}
//..
//.. other methods
}
This class has test methods which now look more closer to the domain, with all mock controls refactored away to the scaffold class. Do you think the domain guys will be able to add more methods to add to the richness of coverage ? The accrued interest calculation is actually quite complicated with more collaborators and more domain logic than what I have painted here. Hence it is quite natural that we need to enrich the test cases with more possibilities and test coverage. Instead of making separate methods which work on various combinations of days, rate and principal (and other factors in reality), why not DRY them up with parameterized tests ?
Parameterized Tests in TestNGTestNG provides a great feature for providing parameters in test methods using the DataProvider annotation. DRY up your test methods with parameters .. here is what it looks like in our case ..
public class AccruedInterestCalculatorTest extends MockControl {
private IAccruedDaysCalculator acalc;
private IInterestRateCalculator icalc;
@BeforeMethod
public final void setup() {
mockControl = EasyMock.createControl();
acalc = newMock(IAccruedDaysCalculator.class);
icalc = newMock(IInterestRateCalculator.class);
}
@DataProvider(name = "test1")
public Object[][] createAccruedInterestCalculationData() {
return new Object[][] {
{20, BigDecimal.valueOf(0.5), BigDecimal.valueOf(1000), BigDecimal.valueOf(30)},
{20, BigDecimal.valueOf(0.5), BigDecimal.valueOf(2000), BigDecimal.valueOf(60)},
{30, BigDecimal.valueOf(0.5), BigDecimal.valueOf(2000), BigDecimal.valueOf(90)},
};
}
private class CalculatorScaffold {
//..
//.. same as above
}
// the test method now accepts parameters
//..
@Test(dataProvider = "test1")
public void normalAccruedInterest(int days,
BigDecimal rate, BigDecimal principal, BigDecimal interestGold) {
new CalculatorScaffold()
.on(principal)
.forDays(days)
.at(rate)
.calculate()
.andCheck(interestGold);
}
@Test(expectedExceptions = {NoInterestAccruedException.class})
public void zeroAccruedDays() {
new CalculatorScaffold()
.on(BigDecimal.valueOf(2000))
.forDays(0)
.at(BigDecimal.valueOf(0.5))
.calculate()
.andCheck(BigDecimal.valueOf(90));
}
}
The test methods look DRY, speak the domain language and do no longer have to administer the mock controls. All test conditions and results are now completely decoupled from the test methods and fed to them through the annotated provider methods. Now the domain guy is happy!