Using literals in your tests can have some advantages, such as, readability and traceability.
While this is true when the data are simple, it's less so when the data are nested, complex structures.
In that case, using literals can hinder refactoring and thus become an obstacle to adapting to changes.
The problem with using literals for complex, nested data is that the knowledge about how to build such data is spread all over the tests. There are many tests that know about the representation of the data.
In that scenario, nearly any change in the representation of those data will have a big impact on the tests code
because it will force us to change many tests.
This is an example of a test using literals, (from a ClojureScript application
using re-frame, I'm developing with Francesc),
to prepare the application state (usually called db in re-frame):
As you can see, this test knows to much about the structure of db.
There were many other tests doing something similar at some nesting level of the db.
To make things worse, at that moment, we were still learning a lot about the domain, so the structure of the db was suffering changes with every new thing we learned.
The situation was starting to be painful, since any refactoring provoke many changes in the tests, so we decided to fix it.
What we wanted was a way to place all the knowledge about
the representation of the db in just one place (i.e., remove duplication),
so that, in case we needed to change that representation, the impact of the change would be
absorbed by changing only one place.
A nice way of achieving this goal in object-oriented code, and at the same time making
your tests code more readable, is by using test data builders which use the builder pattern, but how can we do these builders in Clojure?
Option maps or function with keyword arguments are a nice alternative to traditional builders in dynamic languages such as Ruby or Python.
In Clojure we can compose functions with keyword arguments to get very readable builders that also hide the representation of the data.
We did TDD to write these builder functions. These are the tests for one of them, the db builder:
and this is the db builder code:
which is using associative destructuring on the function's optional arguments to have a function with keyword arguments, and creating proper default values using the :or keyword (have a look at the material of a talk about destructuring I did some time ago for Clojure Barcelona Developers community).
After creating builder functions for some other data used in the project, our test started to read better and to be robust against changes in the structure of db.
For instance, this is the code of the test I showed at the beginning of the post, but now using builder functions instead of literals:
which not only hides the representation of db but also eliminates details that are not used in particular tests, while still being as easy to read, if not easier, than the version using literals.
We have seen how, by composing builder functions and using them in our tests, we managed to reduce
the surface of the impact that changes in the representation of data might have on our tests.
Builder functions absorb the impact of those changes, and enable faster refactoring, and, by doing so, enable adapting to changes faster.
No comments:
Post a Comment