Using the Fluent Builder Pattern to improve test readability and maintenance
Let's assume that we have a class called Order and an OrderService responsible for processing that order. We want to create a test that verifies that after the order has been processed, we get a result back that holds the newly processed orderId. In an ideal world, this test would look something like this:
It's never this easy!
Unfortunately, order processing can be quite complex and in this example the order is not valid to fully process which result in a failed test. For us to be able to fully process this order, we need an order creation that looks something like this:
This is a simple example, in reality creating an order worth processing is usually a lot more complex than this especially when dealing with legacy code.
Now our test is able to process the order and we get a positive test result. However, this raises a few questions:
- What if you have several tests that needs to create orders? We'd need copy & paste these 15 lines of order creation snippet over and over which will bloat our tests.
- What if the order processing behavior one day change? If the order processing behavior should one day change in a way that the order creation need to be updated or extended, all these mentioned tests will need to be updated, which will be a nightmare to maintain.
- In our previous mentioned test the only thing we really care about is the orderId before and after processing the order, so why should we have to read through all of these default dummy values and how do we know which once are important to this specific test?
Why not create a shared setup method?
One way of solving these issues is to move the order creation to a shared setup method in our test class and only accept relevant parameters (in this case the orderId) and assigning default dummy values to all the non-relevant properties. This can look something like this:
This will make our tests a lot slimmer, less repetitive and a lot less fragile to change since there is only one method that would need to be updated. However, this raises another question:
- What if you have several test classes that need to create orders?
Why not create a base class?
Using the above setup we need to copy & paste this setup method in all test classes that handles order creations. What if we move this setup method to a base class and then all our test classes that handles orders can inherit from this base class to access the setup method?
In these situation I usually add the base in front of the setup method so that it is obvious to anyone reading this test that the the setup method comes from the base class and not from this test class.
Now the order setup method is centralized and can be maintained in one place and shared between all test classes that need to handle order creation. This is a perfectly fine approach and one that I usually apply in these situation, however this raises a few other questions:
- What if for some reason I can't (or it doesn't make sense to) inherit from this base class in a specific test class?
- Am I introducing too much dependency between my test classes using this base class approach?
Introducing the Fluent Builder:
According to Wikipedia: "The builder pattern is a design pattern designed to provide a flexible solution to various object creation problems in object-oriented programming. The intent of the Builder design pattern is to separate the construction of a complex object from its representation."
Using this pattern we create an test object builder that will centralize redundant (and sometimes complex) logic that we don’t want to have in each and every one of our tests.
This is done using fluent style programming and it makes our test a lot more readable. Using this fluent style builder we can create orders using something like this:
While this is a lot nicer to read, we're still assigning values to a bunch of properties that are not really relevant to our test. (We only care about the orderId remember?)
Set defaults!
We can add a parameter to the builder called setDefaults and if true, this will set all default values for us so that we only need to focus on what is actually important for our test. This default behavior could look like this:
Now we can reduce all the non-relevant setup from our test and only focus on what is important in this state and condition:
And since the default state is optional, anyone using this builder can decide for themselves if they need the default setup or if they prefer to setup an order themselves to suit their need.
Additionally, they can also choose to set the default values and then orderride any specific property relevant to that test.
Summary:
Using The Builder Pattern together with a default setup we can build an object and hide (centralize) the setup complexity while we focus on what is actually important for each individual test. Also if the behavior would one day change that would affect the default setup somehow we only need to update this logic in one single place.
Hope you enjoyed it.
Cheers friends! ❤️