Monday, 3 May 2010
The importance of a fluent interface for building testdata
Today, I was talking to someone about how we made a fluent interface/dsl that allows us to construct data needed for our scenario tests in a readable and maintainable way. This allows us (the developers) to quickly create and understand tests, and even allows us to explain them to a business analyst when discussing a requirement/bug/current behaviour.
He asked me to clarify what I meant with readable, and since talking about code without seeing any is pretty hard, I'm showing an example here.
The core of our datamodel needed for calculating immovable property taxes consists of the following connected entities (a simplification of the real model):
As you can see, that's already quite a bit of data we will need to setup.
Since this datamodel is also bitemporal (data has a validity and record dimension to it) and sourced (meaning several sources/reporters can report the same kind of data), setting things up gets complex and verbose very quickly.
This is what a (partial) setup would look like in our raw entity model API:
Person landlord = Person.create();
landlord.getPersonalEvent(Reporter.RR, PersonalEventType.BIRTH)
.set(new PersonalEvent(PersonalEventType.BIRTH, new Day(7, 6, 1980), Reporter.RR),
ValidityRange.fromToday());
landlord.getPersonName(Reporter.RR).set(new PersonName("landlord", Reporter.RR));
landlord.getExternalIdentification(Reporter.AKRED, ExternalIdentificationType.AKRED_NUMBER).set(
Collections.singleton(new ExternalIdentification(ExternalIdentificationType.AKRED_NUMBER,
"1234567890X", Reporter.AKRED)));
landlord.getOccupiedSpace(Reporter.VLABEL).set(new OccupiedSpace(someSpace, landlord, Reporter.VLABEL));
CadastralArticle article = CadastralArticle.create(someCadastralDepartment, 1);
Collection<RightState> rightstates = CollectionFactory.newList();
RightState rs = new RightState(landlord, 1, new Percentage(100));
rightstates.add(rs);
article.getTimeSlice(Reporter.AKRED, null).set(new TimeSlice(Reporter.AKRED, rightstates),
ValidityRange.wholeYear(2009));
ImmovableProperty ip = ImmovableProperty.create(someCadastralDepartment, "some-long-code");
Collection<ImmovablePropertyIncome> incomes = CollectionFactory.newList();
ImmovablePropertyIncomeBuilder builder = BuilderFactory.createBuilder(ImmovablePropertyIncomeBuilder.class);
builder.setCadastralIncome(new MonetaryAmount(100));
builder.setFiscalStatus(FiscalStatusType.NORMAL);
builder.setSequenceNumber(1);
builder.setType(ImmovablePropertyIncomeType.NORMAL_BEBOUWD);
incomes.add(builder.build());
ip.getIncomes(Reporter.AKRED).set(incomes, ValidityRange.wholeYear(2009));
// and it just goes on and on
As you can see, that's quite some code to setup the data (and it's just a trivial scenario). Just imagine trying to maintain hundres of testcases written this way. Now, this is what it looks like in our simplified API:
// we need the finals since they're being used in the anonymous inner classes we create.
final Person landlord = new TestPersonBuilder() {
{
bornOn(new Day(7, 6, 1980));
name("landlord");
eid("1234567890X");
occupies(someSpace);
}
}.build();
final CadastralArticle article = new TestCaBuilder() {
{
articleNumber(1);
fullyOwnedBy(landlord);
}
}.build();
ImmovableProperty house = new TestIpBuilder() {
{
plotCode("some-long-code");
normalIncome(100);
includedIn(landlordArticle, sequence(1));
equivalentWith(someSpace);
}
}.build();
This is already quite a bit more readable, and at the same time offers more functionality. Sane defaults are being used behind the scenes, but you can still override them where necessary. By using these TestDataBuilders (as we called those classes), we've been able to implement hundreds of testcases in a readable and maintainable way, and it was much worth the effort coming up with those.
Posted by at 11:01 PM in Java
