Following 3 rules of TDD sounds really simple at first. In practice, there is a moment when one has to implement the whole algorithm at once to make currently failing test pass. This is called “getting stuck” in TDD. In this article, we will explore how exactly this happens and how to prevent that.
Code examples today will be in Ruby programming language. The technique itself is, of course, language-agnostic.
TL;DR
“Getting stuck” happens for a couple of reasons:
- wrong order of tests
- production code is not getting more general with each test
This is a series of articles:
- Part 1: Example (reading this)
- Part 2: Buggy Code and Forcing Our Way Through
- Part 3: Triangulation to the Rescue!
“Getting Stuck” in TDD
Usually “Getting Stuck” follows this pattern:
- write some test and implement it via “simplest thing that might possibly work”,
- write another test and implement it again in a non-general manner,
- write some more tests in that fashion, while never addressing the fact that production code now looks completely wrong from what it should probably be looking like,
- write a new test, that forces us to completely rewrite production code in a complete algorithm just to make it pass.
This last step usually takes minutes to hours depending on the complexity of the problem at hand. Additionally, the first few tests are basically wasted time since they did not produce any bits of knowledge in the production code that persisted in production code in the end. Even worse, chances are that the algorithm that we have just written is not fully covered by current tests, since we have written it in one go just to make current failing test pass - this is no longer correct TDD and can not guarantee high test coverage, and, therefore, can not guarantee high confidence anymore.
Let’s go through a small example on how one can get stuck in TDD:
Order Kind Validation - Getting Stuck
Let’s define the problem at hand first. We have some sort of order request as an input to our system and we need to validate that its kind is correct:
- valid order kinds:
private
,corporate
,bundle
, - order kinds can be combined,
private
andcorporate
order kinds can not be combined, otherwiseInvalidOrderError
with messageOrder kind can not be 'private' and 'corporate' at the same time
,- either
private
orcorporate
should be always present, otherwiseInvalidOrderError
with messageOrder kind should be 'private' or 'corporate'
, - if order kind is not in the above list, then we need to raise
InvalidOrderError
with messageOrder kind can be one of: 'private', 'corporate', 'bundle'
, - if order kind is not present or an empty string, then we need to raise
InvalidOrderError
with messageOrder kind can not be empty
.
This is a fairly simple problem and it is easy to get stuck while doing TDD here. So let’s write our first test: “When order has no order_kind, then we should get InvalidOrderError with message ‘Order kind can not be empty’”:
1 2 3 4 5 6 7 8 |
|
And the simplest implementation possible:
1 2 3 4 5 6 7 8 |
|
Next test is our next simplest edge case - when kind’s value is nil
:
1 2 3 4 5 6 |
|
It does not fail at all, so we don’t have any reason to change the production code. We can already spot a little duplication - validator
variable. Let’s extract it as a named subject of the test suite:
1
|
|
And OrderKindValidator
can be replaced with described_class
(RSpec feature), so that we will not have to change too much in case we wanted to change name of the class:
1
|
|
Next simplest edge case - when kind is an empty array:
1 2 3 4 |
|
I believe I am spotting annoying pattern now:
1 2 3 4 |
|
It would be really nice to write it in this fashion:
1 2 3 |
|
And as another duplication piles up:
1 2 3 4 5 |
|
Now the next tests look very easy and simple:
1 2 3 4 5 6 |
|
And they all pass right from the go. The implementation for the it_fails_with
is looking like this:
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 |
|
So, let’s write our next edge case - when order kind is invalid:
1 2 |
|
Pretty neat! And oh, it fails:
1 2 3 |
|
And the fix:
1 2 3 4 5 6 7 8 9 |
|
Let’s write our next test - when order kind is private
:
1
|
|
This fails as expected with expected no Exception, got #<InvalidOrderError: Order kind can not be empty>
. And to make it pass we need to wrap second raise
statement in the if
condition:
1 2 3 |
|
The implementation for it_does_not_fail
looks like that:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
|
Let’s write our next test:
1
|
|
And it fails with the expected error: expected no Exception, got #<InvalidOrderError: Order kind can not be empty>
. The fix is to amend our if
condition with that case:
1 2 3 4 |
|
And the tests pass. Our next business rule is that one of private
and corporate
should be always present:
1 2 |
|
As expected the test fails:
1 2 3 |
|
And to fix it we just need to sprinkle another if
statement in the middle of the function:
1 2 3 |
|
As expected, the test passes. Now we should test the next business rule - order can not be of private
and corporate
kind at the same time:
1 2 |
|
This, as expected, fails with error message:
1 2 3 |
|
And easiest way to fix that is to add another if
statement:
1 2 3 4 5 |
|
And it passes. Let’s test that we can combine private
or corporate
with bundle
order kinds:
1
|
|
And it fails with error: expected no Exception, got #<InvalidOrderError: Order kind can not be empty>
. To fix this we will have to amend our last if
condition in the function even more:
1 2 3 4 5 |
|
And the test passes. Let’s refactor the code a bit:
- First, we should extract
order[:kind]
duplication to a local variablekind
- Extract common parts of
raise
statement to the private method
After this, OrderKindValidator
will look a bit cleaner:
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 |
|
Let’s write our next test for the same business rule (now a corporate bundle):
1
|
|
And it fails with error: expected no Exception, got #<InvalidOrderError: Order kind can not be empty>
. To fix this we need to add && kind != %w(corporate bundle)
to our last if
condition again.
The code can be found in GitHub repository in an open pull request here.
Now it seems that we have implemented all the business rules (we have all tests for them). Or did we?
Bottom Line
Buggy if
-riddled code is what we’ve got. We will see why in the next part of “Getting Stuck While Doing TDD” series. Stay tuned!
This is a series of articles:
- Part 1: Example (reading this)
- Part 2: Buggy Code and Forcing Our Way Through
- Part 3: Triangulation to the Rescue!
Today we have implemented our not-so-complex problem at hand while following 3 rules of TDD. The result was not of the best quality and we will take a look why in further articles of these series. You would not want to miss next articles on this tech blog, we still have a lot to talk about:
- Triangulation technique in Test-Driven Development - overlooking this technique might cause one fail at doing TDD (these series),
- 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, 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.