So, by using lambdas in the Fluent Builders our tests started containing expressions such as:
We found that this improved the readibility of tests, making them a bit more intelligible by non-technical people.
While the goal was not to implement BDD tests – which are more conveniently written with specialized libraries – we felt that the tests could benefit from having an even more fluent syntax, possibly using language closer to domain than to the technical implementation.
We found a confirmation in Martin Fowler’s FluentInterface post:
Probably the most important thing to notice about this style is that the intent is to do something along the lines of an internal DomainSpecificLanguage. Indeed this is why we chose the term ‘fluent’ to describe it, in many ways the two terms are synonyms. The API is primarily designed to be readable and to flow. The price of this fluency is more effort, both in thinking and in the API construction itself. The simple API of constructor, setter, and addition methods is much easier to write. Coming up with a nice fluent API requires a good bit of thought.
We tried to go toward the direction of DSL, applying the following approaches.
Why not replacing the new
statements with Factory Methods? In the simplest cases, the result is just an innoquous, cosmetic change, such as:
The implementation is trivial: create a Factory Method, add a using static
and make the constructor private:
In general, the use of Factory Methods offers the opportunity to build pre-filled entitities while still using expressive statements that speak the domain language. For example, one could feel the need to define AnInvalidOrder()
, AnAlreadyProcessedOrder()
or the like (see Use a dedicated Factory Method to pre-fill values).
Methods need not to be be named after the entity’s fields. In our example, it may have sense to prefer the names on the right:
Previous name | New name |
---|---|
WithDate() |
CreatedOn() |
HavingCustomer() |
CreatedBy() |
HavingArticle() |
ContainingAnArticle() |
WitName() |
Named() |
WithCategory() |
InTheCategory() |
These changes can be safely performed with no other side effects, and to the benefit of the overall readibility:
A builder method can be decoupled not only from the underlying field name, but also from its value or its format.
Say for example that the customer age is relevant for a test, and that Customer
entity has a field to store the birth date:
It is trivial to add a WithBirthday()
– or a BornOn()
– method in CustomerBuilder
:
The entity would be built with:
But maybe the test is focused on the age of customers, rather than on their birtday, for example to test the different paths taken whether the customer is an adult or not. In this case, it might have sense to replace BornOn()
with Aged()
, and write test like:
Aged()
could be easily implemented with something like:
I would be very cautious in putting too much logic in the builder. Builders are supposed to be syntactic sugar around constructors and setters, and they should not contain too much magic.
Maybe a domain concept affects more than one value. Say for example that the order comprises 3 fields: one to mark it as a gift, one for including the invoce in the box and a last one to print the sender name.
It might have sense to define methods such as AsAGift()
that affects both the values, if it helps the test to be more concise and expressive:
The same considerations of the previous paragraph stand: I would not push this too far. The builder shouldn’t be to opaque, and most often than not, explicit is better than implicit. Concisiness is not always a benefit for tests readibility.
From time to time we found that some values were needed by the implementation, but they were in fact not relevant at all to describe the test case. The rule of thumb is to reference only the values that are useful to describe the test case, and to set all the irrelevant (but needed) fields to some default values.
For example, if the test is about books, it would be nicer to have a code like:
rather than:
In the former case, when the entity is built a lot of information, completely irrelevant to the specific test case, is mentioned. The resulting test is harder to understand and in general less expressive.
Yet, this information is needed, so the builder cannot refrain from filling all the needed entity’s fields.
We found 3 options:
The idea is: when the Builder is invoked, it puts the entity in a known and valid state. For example:
would create an order with some default date, with an article having some default price, a customer with a default name and so on. The subsequent methods can modify the values at need. If a test needs to exercise some code relative to the creation date, it would do well to mention the date as following:
If another test focuses on the article, it can just omit the information about the creation date, relying on the fact that Builder will create a valid order anyway.
To implement this, just add the default value in the Factory Method:
Of course, the Factory Method can itself use the fluent interface:
The previous trick can hide too much information and make the test too opaque. It can make sense to have more than one Factory Method, with expressive names that clearly conveys their intent, such as AnEmptyOrder()
or AnOrderWithOneArticle()
, so tests can focus only on the relevant information:
Again, the implementation is pretty simple:
In other words, it could make sense to move into the builder itself some of the expressions that happen to be repeated in the tests and that add no valuable information to the specific test cases.
We found thou that abusing this approach can make the tests a bit opaque, as the Factory Methods hide too much information.
As an alternative, we found it convenient to have methods such as WithOneArticle()
that fill the entity withoot requiring the programmer to specify values (non relevant for the test case), without making the test too implicit:
Adopting this approach we found that sometimes the complexity introduced by the builders can overtake the benefits of readibility and domain-focused tests. This can happen especially if the builders exceed with magical methods, if they have too implicit behaviours and if they hide too much information. We also found that the biggest risky methods are the one that tend to have overlapping side effects, and that can become hard to combine in a single expression.
For this reason, we try to keep the builders as simple as possible (but not simpler). In other words, we know these techniques are tools at our disposal, but we apply them conscientiously. We mostly focus on tests, not on builders: builders are meant to ease the test writing, and they are supposed to be reused in a very high number of tests, without modifications. When we feel the need to enhance or modify a builder, it happen that the modification causes controversies and discussions. In all the cases, we tend to prefer simplicity to completeness.
A good rule of thumb is the one suggested by Leonardo:
When you feel that the builder code would benefit from being test covered, it’s the sign it has gone too far.
Find here the complete example.
ciao!