The dev team at Vendorful take code quality seriously. We invest in writing tests for every bug fix and new feature being implemented and in well reviewed merge requests. As any good developer should know from their past experience that with good test coverage they can actually move faster as a team in the long run. However, in addition to having good test coverage, writing human-readable code and tests is equally important. Personally, my feeling of having to work with a neatly nested and organized test suite set up but extremely difficult to follow is almost comparable to having to walk through a wild maze to find something that is not very worthwhile, every time.
Coming from Ruby on Rails background, I used to use RSpec to write my tests. While coding with Elixir and using ExUnit to write tests nowadays, I caught myself finding ways to nest RSpec
context blocks inside each other. Let's find a simple example: you wanted to build a chef robot to prepare you dinners base on the left over food in your fridge. Depending on what's available in your kitchen, it'll choose how to prepare the appropriate dishes.
This is how I would write TDD tests in RSpec.
As you can imagine, if these tests are all filled up and more nested contexts are added, such as: microwave settings depending on size of dishes, stove top heat amount and time, etc… it will become too long and too complicated to read and follow, especially for other devs who haven’t worked on this area of the robot. Also, there is no way to write nested “contexts” like this in Elixir’s ExUnit — and there is a good reason for that.
By forbidding hierarchies in favor of named setups, it is straightforward for the developer to glance at each describe block and know exactly the setup steps involved. — ExUnit docs
With ExUnit, what you can, and want to do is to use named setups.
What are named setups?
I have to admit that the documentation for named setup in
ExUnit is kind of buried somewhere in the middle of
ExUnit.Case documentations. Named setups are simply configurations of tests used to put together test data for the following tests. Each setup is actually just a function that configures the test data and returns the variables to be used in the tests themselves. If you are familiar with writing tests in
ExUnit then these configuration functions are exactly the
setup blocks. For example:
Note that a
context variable is passed in this setup function so that the previous configurations can be accessed in this setup as
context.size in this case. You can also grab the
size variable out of this
context right in the function signature like so:
How do I use it in the tests?
Let’s make it even nicer by combining the condition “seasoning available”:
Note that if you have multiple setup functions in sequence like this, the later ones on the right can read the “context” variables returned by the previous ones on the left. So
:set_seasoning can actually access
cold_dish variables from
:setup_cold_dish but we just not doing anything with them in this scenario.
Now let’s write the whole thing with combined conditions!
Do the tests look flattened? Yes. And I have to admit that they are much easier for other developers (and of course for me at a later time) to read and follow at a glance. And the beauty of it is: no more nested context/describe blocks, no more mazes, shared setup functions and cleaner code!
Please check out my other Elixir posts if you are interested!