Welcome back to the “Getting Stuck While Doing TDD” series. Today we are going to learn the Golden Rule of TDD and how to not get stuck while doing TDD.
TL;DR
- “As tests get more specific, production code gets more generic”.
RED
is as important as other in Red-Green-Refactor cycle. If next test does not fail, it is either: already implemented, or has to wait until a later time (until it will fail).At its core the Triangulation Technique has the following idea:
After implementing one business rule (with Red-Green-Refactor) make sure to find all “weirdnesses” or non-generalities in the production code and one-by-one eliminate them by writing a test, that proves such non-generality, and then making it pass while removing non-generality. This is the third cycle of TDD - Mini Cycle.
This is a series of articles:
- Part 1: Example
- Part 2: Buggy Code and Forcing Our Way Through
- Part 3: Triangulation to the Rescue! (reading this)
Shall we get started?
Specific/Generic Rule of TDD
As tests get more specific, production code gets more generic.
When making the next failing test pass, our production code should also pass a whole class of similar tests. Best shown in the very simple example. The task at hand is to write the function sum(a, b)
that will add two numbers. Let’s see us a violation of the Specific/Generic rule:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
|
The production code to make this last test pass is as specific as the failing test now. The test of the same class (where we change the value of the b
parameter) will fail for it:
1 2 |
|
To follow the Specific/Generic rule we ought to make 4
into 2 + b
like that:
1 2 3 4 |
|
This way, when we change b
to any value it will still pass the test, aside from the fact, that we didn’t do anything about a
. This is because we still don’t have any test showing us, that parameter a
is important, like the following one:
1 2 |
|
Again we can make it pass in a very specific fashion by introducing specific if
statement, or we could do it to pass the whole class of such tests:
1 2 3 4 |
|
Have you noticed, that from the test suite side we had to “prove” that some knowledge in the system is important and had to be used? This technique is called Triangulation.
Triangulation Technique
In the essence, Triangulation technique has a very simple idea at its core:
- Change certain important* knowledge in the system.
- Assert that the production code behaves in an accordingly expected manner.
* - important from the perspective of the system or unit under the test
Red-Green-Refactor has to have all stages
One Red-Green-Refactor cycle really has to have all stages in it. And I’m not ranting right now about “Refactor” stage, that is a given. Rather, I insist on the “Red” stage - in TDD, when we write a new test, it has to fail. Writing tests that do not fail is another way to get ourselves stuck while doing TDD. One could ask: “If I can’t write this test because it does not fail, what should I do about the requirement it represents?”, and the answer is rather simple - either this requirement is already implemented and tested by other tests, or we still need this test and we will get back to it later when it actually will fail.
As we can remember, in the first part of these series, we were going through an OrderKindValidator
example, and we were writing multiple tests in a row, that were all expecting the same outcome and of course they didn’t fail, because we had one line in our function that made them all pass. If we were to sprinkle some other tests, that do fail (like a test for a valid order kind), after making it pass, all of these tests will now be failing and therefore they are good candidates for our next test. Let’s see it with our own eyes:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
|
Now is the point, where we have to choose our next test, and last time we have chosen the test with the same outcome and it did not go so well. Let’s choose a test with different outcome, e.g.: when valid order kind is provided:
1 2 3 4 |
|
Now, we have 2 options, to either check for order[:kind] == %w(private)
or to check for order[:kind]
being absent. It does not matter what we choose at this point, so let’s go with the first one:
1 2 3 4 5 6 7 8 |
|
Now let’s apply Triangulation technique. We should always ask ourselves the question: “What is weird about this code?” and “What failing test should I write to point out this weirdness?”. First weirdness we can spot is that the validator currently accepts only one order kind - private
. According to our requirements it should also accept corporate
:
1 2 3 4 5 6 7 8 9 10 11 |
|
We also know, that our system should handle duplicate entries in order[:kind]
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Wow! We, of course, can check for kinds
to not be nil
, but I would rather listen to this test failure and put a check for kinds
being absent (and this makes for our second check, that we could have chosen from):
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
So this passes all our tests. It may look weird, and this is exactly the pointer for us which test to write next to prove, that this weirdness is incorrect:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
Production code starts looking not so clean and I think it is time to give things proper names:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
|
There is only one weirdness, that is left for triangulation in current production code, before we can move on to the next requirement - private
can be duplicated while corporate
can not:
1 2 3 4 5 6 7 8 9 10 |
|
Great, now we can safely go back to our empty order kind edge cases:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 |
|
And it is a good opportunity to eliminate some duplication:
1 2 3 4 5 6 7 8 |
|
Now, it is a good time to triangulate, because we have a weirdness in our code: kinds[0]
. To prove that this is too specific we can write another test:
1 2 3 4 5 6 7 8 9 10 |
|
Notice, how every single test that we have written was failing and how easy it was to make it pass. This suggests that we are probably moving in the right direction. Let’s test our next requirement - we can combine private
and bundle
:
1 2 3 |
|
Wait a minute. This is really bad. We should have a failing test here. This happened because we are checking only for the inclusion of private
or corporate
and we do not care about anything else in the order[:kind]
array. We have to discard this test and try to go with failing version of the same business rule - invalid order kind can not be combined with private
:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
While this works, it leads to two other weirdnesses: kinds[1]
and "invalid"
, let’s the latter first:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
Other tests fail now, from them it is possible to see, that second kind should be either private
or corporate
:
1 2 3 4 5 6 7 8 9 |
|
This looks rather clunky, we should make it a bit cleaner:
1 2 3 4 5 6 7 8 9 |
|
Let’s eliminate the other weirdness - kinds[1]
, it probably should verify all kinds in the array:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
And now this can be greatly simplified by inverting the boolean logic:
1 2 3 4 5 |
|
Now that we have dealt with all weirdnesses in our production code, let’s get back to our requirement:
1 2 3 4 |
|
Wow! Now it fails exactly as it should. This means that it is now the right time for this test! Let’s make it pass by adding bundle
to the list of allowed order kinds:
1 2 |
|
Nice! Our next requirement is about bundle
not being used on its own, i.e.: either private
or corporate
is required:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
And this is good enough, because that is really the only case, when this can happen until the list of allowed order kinds is extended by future business requirements. We should at least give this condition a proper name:
1 2 3 4 5 6 7 8 9 |
|
Except, that we could provide duplicated bundle
:
1 2 3 4 5 6 7 8 9 10 11 |
|
Now it is time to move on to the final requirement about conflicts between private
and corporate
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
|
Of course, kinds == %w(private corporate)
can be considered too specific for production code, we should triangulate it:
1 2 3 4 5 6 7 8 9 10 11 |
|
And, finally, let’s give this condition a proper name:
1 2 3 4 5 6 7 8 9 10 |
|
I believe we are done now. Source for this example can be found in an open pull request here.
Let’s recap how Triangulation technique worked for us here.
Triangulation Technique in Depth
The main goal of triangulation is to prove that the code is not general enough along some axis (class of tests) by writing a test and then making sure it passes. Effective application of the technique requires to prove and eliminate all such “weirdnesses” or non-generalities from the production code after each Red-Green-Refactor cycle for business requirements. This is, in fact, the 3rd cycle of Test-Driven-Development called Mini Cycle of TDD, it should be executed about every 10 minutes.
Another observation is that following this technique we are introducing only one small piece of knowledge into our production code, for example:
- When writing a test for next business requirement, we are introducing the fact that we need an
if
statement with a certain body (in this example it was araise error
statement). Since we can not introduce theif
statement without a condition we need to put some condition there and we put a very specific condition on purpose since we know that it is tested and it is simple. - Next, we are proving that this condition is too specific by writing a test, and then making it pass with a more generic solution. This way we are introducing a tiny little bit more knowledge in our production code.
- We are repeating this iterative process until the production code is generic enough for the current specification (test suite). And we start over. This is the Mini Cycle of TDD.
Bottom Line
Today we have learned the Golden Rule of TDD - “As tests get more specific, production code gets more generic”, and we have learned the Triangulation Technique, that allows us to follow this rule in an incremental and confident way. Additionally, we have learned, that following Red-Green-Refactor strictly is important, and this includes even the RED
stage of this cycle - when the test for business requirement does not fail, it is either: already implemented or it has to wait for later.
This is a series of articles:
- Part 1: Example
- Part 2: Buggy Code and Forcing Our Way Through
- Part 3: Triangulation to the Rescue! (reading this)
You would not want to miss next articles on this tech blog, we still have a lot to talk about:
- Continuous Integration and Continuous Delivery - importance of not impeding others,
- Open-Closed Principle - changing behavior by adding new code,
- Mutational Testing, “Build Your Own Testing Framework” series, 4 Cycles of TDD, Test-Driven Development screencasts and so much more!
Thanks!
Thank you for reading, my dear reader. If you liked it, please share this article on social networks and follow me on twitter: @tdd_fellow.
If you have any questions or feedback for me, don’t hesitate to reach me out on Twitter: @tdd_fellow.