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 Test
The 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 TestNG
TestNG 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!
4 comments:
If you're not writing tests before the code you're not doing TDD, you're doing testing. The very definition of TDD is that you write the tests first to drive the development of code to make those tests pass.
The rest is cool stuff though.
I admit that it is not TDD from the purists' point of view, which I have admitted. Once I have determined at least the collaborators and the major public contracts, I start writing tests. From then onwards, it is an iterative cycle of red-green-refactor. I am not sure what percentage of developers do *pure* TDD by the books.
Great article - as usual - there are a couple of things that bug me about your site layout though...
It's limited to what looks like 800*600, resulting in a lot of asted screen real estate - this is particularly annoying when the blog entry contains code examples and horizontal scrolling is required in order to view the text.
Considering that your target audience is almost guaranteed to be developers, and that any developer still working in 800*600 deserves to be shot, how about widening things up to 1024 at least.
Secondly, how about adding some printer orientated stylesheet entries so that the navigation panel isn't shown on paper. This would partly address the first issue and remove clutter from the printout.
Keep up the good work.
Bob
情趣用品,情趣用品,情趣用品,情趣用品,情趣,情趣,情趣,情趣,按摩棒,震動按摩棒,微調按摩棒,情趣按摩棒,逼真按摩棒,G點,跳蛋,跳蛋,跳蛋,性感內衣,飛機杯,充氣娃娃,情趣娃娃,角色扮演,性感睡衣,SM,潤滑液,威而柔,香水,精油,芳香精油,自慰套,自慰,性感吊帶襪,吊帶襪,情趣用品加盟AIO交友愛情館,情人歡愉用品,美女視訊,情色交友,視訊交友,辣妹視訊,美女交友,嘟嘟成人網,成人網站,A片,A片下載,免費A片,免費A片下載愛情公寓,情色,舊情人,情色貼圖,情色文學,情色交友,色情聊天室,色情小說,一葉情貼圖片區,情色小說,色情,色情遊戲,情色視訊,情色電影,aio交友愛情館,色情a片,一夜情,辣妹視訊,視訊聊天室,免費視訊聊天,免費視訊,視訊,視訊美女,美女視訊,視訊交友,視訊聊天,免費視訊聊天室,情人視訊網,影音視訊聊天室,視訊交友90739,成人影片,成人交友,美女交友,微風成人,嘟嘟成人網,成人貼圖,成人電影,A片,豆豆聊天室,聊天室,UT聊天室,尋夢園聊天室,男同志聊天室,UT男同志聊天室,聊天室尋夢園,080聊天室,080苗栗人聊天室,6K聊天室,女同志聊天室,小高聊天室,上班族聊天室,080中部人聊天室,同志聊天室,聊天室交友,中部人聊天室,成人聊天室,一夜情聊天室,情色聊天室,寄情築園小遊戲情境坊歡愉用品,情境坊歡愉用品,情趣用品,成人網站,情人節禮物,情人節,AIO交友愛情館,情色,情色貼圖,情色文學,情色交友,色情聊天室,色情小說,七夕情人節,色情,情色電影,色情網站,辣妹視訊,視訊聊天室,情色視訊,免費視訊聊天,美女視訊,視訊美女,美女交友,美女,情色交友,成人交友,自拍,本土自拍,情人視訊網,視訊交友90739,生日禮物,情色論壇,正妹牆,免費A片下載,AV女優,成人影片,色情A片,成人論壇,情趣,免費成人影片,成人電影,成人影城,愛情公寓,成人影片,保險套,舊情人,微風成人,成人,成人遊戲,成人光碟,色情遊戲,跳蛋,按摩棒,一夜情,男同志聊天室,肛交,口交,性交,援交,免費視訊交友,視訊交友,一葉情貼圖片區,性愛,視訊,視訊聊天,A片,A片下載,免費A片,嘟嘟成人網,寄情築園小遊戲,女同志聊天室,免費視訊聊天室,一夜情聊天室,聊天室
Post a Comment