Using a DSL to create a fluent interface for unit testing your domain model
Martin Fowler's recent post on ExpressionBuilders had me smiling as we ran into a similar problem at work last year. It is great to see that our solution closely mirrors the ExpressionBuilder solution he writes about. We even named our classes Builders as well
Anyway, I've been wanting to blog about it for quite a while now.
Problem
The problem we needed to solve was how to effectively unit test our domain model that we created using the principles highlighted in Domain Driven Design. The domain model is quite big and we needed the domain model to be populated with different test scenarios to run our unit tests to verify all the hundreds of business rules that we have coded. When I say hundreds I am not joking as we are modelling complex life insurance products.
Solutions
The first approach we considered involved scripting the test cases into SQL and using a tool like DataFresh to load the test cases into our database sandbox, executing the tests and then reverting the database tables back to their original state at test tear down. We also considered MbUnit's support for running the tests in a COM+ transaction to isolate the database changes. This solution had quite a few problems in our minds:
- Very cumbersome to create the test cases in SQL script. It is easy enough to create the script files once you have a populated database using a tool like Apex SQLDiff, but getting the data into the database is the problem.
- Slow tests due to integration with the database from within the unit tests
- Dependence on the database within unit tests
- Test data brittleness due to domain model changes. Any change to the domain model could potentially result in the test data being invalidated. No intellisense or compile time errors to highlight these kind of errors.
We decided against using this approach.
The second approach we considered was using a test pattern called ObjectMother[1]. The ObjectMother is a kind of Test Helper that you code:
"One of the most time-consuming and unfulfilling activities in testing is coding and maintaining test objects. The ObjectMother pattern addresses this issue by proposing a simple, scalable framework whose sole purpose is to construct and facilitate the customizations of test objects. The pattern’s primary responsibilities are: to create test objects, to tailor those objects at any point during the testing process, and to terminate those objects once testing is complete. When the process of test-writing is made easier and the quality of test data more consistent, developers are likely to write more and better tests." [1]
This solution addresses all of the problems of the first solution. No database dependence, quick running tests, compile time checking of domain model changes due to fact that the ObjectMother is POCO so we decided on using this approach. This resulted in code looking something like this:
public static class PersonObjectMother
{
public static Person CreatePerson(Title title, string surname, string firstName, string initials, string idNumber, DateTime dateOfBirth, Gender gender, MaritalStatus maritalStatus, CorrespondenceLanguage preferredLanguage, decimal monthlyIncome)
{
Person person = new Person();
person.Title = title;
person.Surname = surname;
person.FirstName = firstName;
person.Initials = initials;
person.IdNumber = idNumber;
person.DateOfBirth = dateOfBirth;
person.Gender = gender;
person.MaritalStatus = maritalStatus;
person.PreferredLanguage = preferredLanguage;
person.MonthlyIncome = monthlyIncome;
return person;
}
public static Person CreateHennieLouw()
{
Person hennieLouw = CreatePerson(Title.Mr, "Louw", "Hennie", "H", "7108070108083",
new DateTime(1971, 8, 7), Gender.Male, MaritalStatus.Married, CorrespondenceLanguage.English, 10000M);
AttachOccupation(hennieLouw, "Software Developer", "Microsoft Ltd", "Pinelands");
AttachCellNumber(hennieLouw, "+27", "82", "4827");
AttachHomeNumber(hennieLouw, "+27", "21", "9814402");
AttachPostalAddress(hennieLouw, "6 Sinai Close", "Cape Town", "8000");
AttachHomeEmail("hennie.louw@gmail.com");
return hennieLouw;
}
The code snippet illustrates a simplified version of a PersonObjectMother that contains some creation methods to create populated Person objects. It provides a parameterized creation method to allow construction of any custom Person object but also provides a named state reaching method for creating a well-known object instance for a pseudo Hennie Muller person. This object mother contains some various other attachment methods (not shown here for brevity sake) to create more fully populated Person as illustrated in the CreateHennieLouw method.
As our domain model grew we inevitably had to refactor some of the ObjectMothers' creation and attachment methods and after a while creating and maintaining these was becoming a real pain. The granularity of the methods often caused problems with a change to a creation/attachment method flowing throughout all the test cases and all the unit tests that were using them. I started wondering whether there wasn't a more elegant solution for creating test cases for our domain model.
More elegant solution
There has been a lot of focus in the last year or two on Domain Specific Languages (DSL's) and fluent interfaces. At work we use TypeMock as our mocking framework and it provides a nice fluent interface for setting expectations on the mock objects. We started thinking whether it was not possible to create a fluent interface to replace our ObjectMothers - a mini DSL that creates test cases for our domain model but is readable and flows. It would have a lot of small creation/attachment methods as part of the DSL which could be chained together to create the test cases. After some work we ended up with Builder classes that works quite similar to the ExpressionBuilder classes Fowler writes about.
To illustrate, here is a sample of using the DSL to create our pseudo HennieLouw Person:
public static Person CreateHennieLouwUsingDSL()
{
return PersonBuilder.StartRecording()
.CreatePerson(Title.Mr, "Hennie", "Louw", new DateTime (1971, 8, 7), Gender.Male)
.PrefersToSpeak(CorrespondenceLanguage.English)
.WithCitizenshipDetailsOf(EthnicGroup.White, "7108070108083")
.WithMaritalStatusOf(MaritalStatus.Married)
.WithPrimaryJobOf("Software Developer")
.WorksAt("Microsoft Ltd", "Pinelands")
.EarnsMonthlyIncomeOf(10000M)
.WithCellNumberOf("+27", "82", "2824827")
.WithHomeNumberOf("+27", "21", "9814402")
.WithPostalAddressOf("6 Sinai Close", "Cape Town", "8000")
.WithHomeEmailOf("hennie.louw@gmail.com")
.Finish();
}
Here are some code snippets of the PersonBuilder.
public sealed class PersonBuilder
{
private Person person;
#region Constructors
public PersonBuilder()
{
person = new Person();
}
#endregion
#region Factory Methods
public static PersonBuilder StartRecording()
{
return new PersonBuilder();
}
public Person Finish()
{
return this.Person;
}
#endregion
#region Properties
private Person Person
{
get { return person; }
}
#endregion
The StartRecording method provides a static factory method that kicks off the whole process of assembling the Person through invoking the constructor. The Finish method returns the populated Person instance. The rest of the PersonBuilder contains the various creation/attachment methods that form the mini-DSL. These can be chained together to "record" the Person instance with the relevant test data. Here is a code snippet of some of the DSL methods:
#region DSL Methods
public PersonBuilder CreatePerson(Title title, string firstName, string surname, DateTime? dateOfBirth, Gender gender)
{
Person.Title = title;
Person.FirstName = firstName;
Person.Surname = surname;
Person.DateOfBirth = dateOfBirth;
Person.Gender = gender;
return this;
}
public PersonBuilder WithCitizenshipDetailsOf(EthnicGroup ethnicGroup, string idNumber)
{
Person.EthnicGroup = ethnicGroup;
Person.IdNumber = idNumber;
return this;
}
public PersonBuilder WithMaritalStatusOf(MaritalStatus maritalStatus)
{
Person.MaritalStatus = maritalStatus;
return this;
}
private PersonBuilder WithAddress(AddressType type, Language language, string line1, string suburb, string code)
{
Address address = Person.AddAddress(type, language);
address.Line1 = line1;
address.Line3 = suburb;
address.Code = code;
return this;
}
public PersonBuilder WithStreetAddressOf(string line1, string suburb, string code)
{
return WithAddress(AddressType.StreetAddress, Language.Unknown, line1, suburb, code);
}
public PersonBuilder WithPostalAddressOf(string line1, string suburb, string code)
{
return WithAddress(AddressType.PostalAddress, Language.Unknown, line1, suburb, code);
}
Extending the DSL is quick as we just add the additional methods or overloads of existing methods. We have different builder classes interacting with each other to create complete test cases for our domain. The test cases are a lot more readable and overall we are a lot more happy with using this approach to the ObjectMother approach.