Approval testing: 3 technics to deal with randomness
Cet article est disponible en français.
When dealing with legacy code, randomness is not something you enjoy meeting.
Creating a test harness on top of a system with random results can be pretty challenging.
Obviously, you’d like your tests to be deterministic and to always give you the same result, provided you didn’t change anything in the system.
Let’s see three ideas that can help you deal with the randomness in the system. The first two technics are based on the idea of removing the random part of the system during tests.
Block the random generator
The first solution I wanted to offer in this article is to ensure that the random generator always returns the same results when called. The answer here is to seed the generator with a value you control, to make the generator work in a deterministic manner.
For instance, in PHP, you can call the mt_rand
function. Later, any call to the rand
function will return the same random numbers in the same order.
This is a fast way to turn the non-deterministic system into something that always gives the same results.
Unfortunately, this solution, while being one of the quickest ways to get the job done, only works in some situations. First, not all languages allow seeding the random number generator. If you can’t, you can’t and need another solution.
Another caveat of this solution is that if you modify the structure of your code and the order of calls to a random method is changed; you’ll get results in the same order but not in the same logical space. For instance, let’s take two calls to rand
and assign the result of the first call to variable A and the second call to variable B. After seeding the random number generator, A and B are always set to the same value, say 34 and 1879. During a refactoring, if we decide to invert the order assignation, we have a problem. The code now assigns values to B before A; A is now 1879, and B is 39. You can play with this idea here.
What can we do when seeding the random number generator is impossible, or we might want to change the order of the calls at some point?
Control the randomness
The other alternative is to take back control of the values generation. As often, adding a layer of abstraction is one of the solutions available in our toolbox. Here we want to break the direct dependency to the random value generator and swap it with a dependency we can manipulate.
In more concrete terms, instead of directly calling a function from the language, we encapsulate it, create a stub we can manipulate, and find a way to substitute the original implementation with our double.
The substitution technics available greatly vary depending on the programming language you’re using and the code’s state. If you’re lucky enough to be able to inject dependencies at construction, this is an easy task. If you’re not, some patterns from Working Effectively With Legacy Code by Michael Feathers can prove helpful.
This solution is available in every language. The only requirement is to feel comfortable messing around with code while not having tests to be able to take control back.
Once you have control, you can decide the values generated.
That solution allows us to solve the ordering issue mentioned before. If you invert the order of calls in the code, you can change the configuration of your stub to change the order and keep the tests passing.
Modifying the stub after tests are created needs to be done carefully. If you want greater confidence and want to avoid touching the tests, you have another alternative. Instead of making one abstraction for every call to the random generator, you can create one for each call. Taking back the example from the first part, you can introduce an abstraction for generating A and an abstraction for generating B. Having two implementations gives you more granular control, and you can create two stubs. When you invert the call order, the code still calls the right abstractions and the same stubs and obtains the same final result.
Touching an existing code base to introduce indirections to control randomly generated value is not always straightforward nor necessary. Sometimes we don’t care about the generated value, and we can be totally fine with keeping the randomness as long as we manage to keep our tests deterministic. And this is the point of the third idea.
Hide the randomness
Sometimes the random value is not really important for the result. If we don’t care about it, we can remove it from what is asserted in the test.
The trick here is that you don’t need to make assertions based on the direct outputs of the system under test. Instead, you can get them and transform them to keep only the information you need to be confident enough that you’re not breaking anything.
When doing approval testing, it is common to use a printer, a function or a class that will transform the outputs to make them look easy to understand. If a piece of information is not relevant, the printer’s role is to remove it and create a clean output. You can then use that clean output in the assertion.
What could be simpler than removing what you don’t care about?
Do you speak French and want to stop hating your tests ?
I've created a course to help developers to improve their automated tests.
I share ideas and technics to improve slow, flaky, failing for unexpected reason and hard to understand tests until they become tests we take joy to work with !
But you'll need to understand French...
Sometimes you don’t care about the actual value, but you want to ensure that a value is here. Instead of removing the random part, you can replace it with something else. Say you don’t care about the value of a UUID but want to be sure that it is displayed. Using a regular expression, you can replace all UUIDs with something else, like a UUID
string.
Another common use case is to detect that a value is repeated in several places in the output.
Let’s continue with the previous example. You still don’t care about the value of the UUID, but you want to assert that the UUID used in a link to access a product is the product’s UUID, not some other one.
Here the printer has to be more clever.
When a UUID is detected using a regex, the scrubber generates a new string, usually made from a base string and an increment (UUID_1
). Then it replaces all the subsequent occurrences of that UUID with the generated string, increments the counter and repeats for each UUID found in the output. That way, even if you can’t predetermine the result of the system, you can turn it into something deterministic that you can use as your golden master.
The good thing about this solution is that it doesn’t require touching the legacy system. You don’t need to worry about breaking something while creating the tests. This is why it is a very convenient and relatively rapid method to build confidence. Also, you might be very lucky and work with a language where approval testing tools come with scrubbers doing part of the cleaning work for you.
Discovering a system using randomness and needing to create a test harness doesn’t have to be scary or take an awful lot of time. With the three ideas exposed in this article, you should be able to attack that piece of legacy code you avoided refactoring for a while.
If you feel like you could use a hand to gain control over a legacy codebase, let's chat and see how I can help.