We recently did the Luhn Test kata at a Barcelona Software Craftsmanship event.
This is a very interesting problem to practice TDD because it isn't so obvious how to test drive a solution through the only function in its public API: valid?.
What we observed during the dojo is that, since the implementation of the Luhn Test is described in so much detail in terms of the s1 and s2 functions (check its description here), it was very tempting for participants to test these two private functions instead of the valid? function.
Another variant of that approach consisted in making those functions public in a different module or class, to avoid feeling "guilty" for testing private functions. Even though in this case, only public functions were tested, these approach produced a solution which has much more elements than needed, i.e. with a poorer design according to the 4 rules of simple design. It also produced tests that are very coupled to implementation details.
In a language with a good REPL, a better and faster approach might have been writing a failing test for the valid? function, and then interactively develop with the REPL s1 and s2 functions. Then combining s1 and s2 would have made the failing test for valid? pass. At the end, we could add some other tests for valid? to gain confidence in the solution.
This mostly REPL-driven approach is fast and produces tests that don't know anything about the implementation details of valid?. However, we need to realize that, it follows the same technique of "testing" (although only interactively) private functions. The huge improvement is that we don't keep these tests and we don't create more elements than needed. However, the weakness of this approach is that it leaves us with less protection against possible regressions. That's why we need to complement it with some tests after the code is written to gain confidence in the solution.
If we use TDD writing tests only for the valid? function, we can avoid creating superfluous elements and create a good protection against regressions at the same time. We only need to choose our test examples wisely.
These are the tests I used to test drive a solution (using Midje):
Notice that I actually needed 7 tests to drive the solution. The last four tests were added to gain confidence in it.
This is the resulting code:
See all the commits here if you want to follow the process. You can find all the code on GitHub.
We can improve this regression test suit by changing some of the tests to make them fail for different reasons:
I think this kata is very interesting for practicing TDD, in particular, to learn how to choose good examples for your tests.
Hi,
ReplyDeleteI didn't know this kata, thanks for the post and showing it to me, I've been playing a bit with it.
As you says, the problem is to have good examples to drive the development. How did you find your examples? Do you know any heuristics to do so? My intuition is that for this kind of development where you know the algorithm from the beginning, it's the algorithm itself that suggests some values (f.i., you have to split between odd and even elements, you have to apply some mappings to each set and add them, ...), but that's not very generic...
Hi Abel,
ReplyDeleteThanks, I'm glad you found it interesting.
Yes, in this case, it's knowing the algorithm that help me find good examples.
I also try to have tests that fail for only one reason, but it's not always possible.
There are other heuristics to find good examples for other type of problems (non-algorithmic ones) like James Grenning’s "ZOMBIES" or Rainsberger's "0, 1, several, ups".
Best regards,
M