Back in 2014 I wrote a blog post listing three mistakes often made by folks who are new to test-driven development (TDD). The three mistakes I identified are:
Starting with error cases or null cases.
Writing tests for invented requirements.
Writing a dozen or more lines of code to get to GREEN.
It was a very long post, so I’ve taken the three parts and expanded each into its own article, also incorporating the comments I received in 2014. This is part 3, and will deal with taking a step that’s too big…
When the bar is red and the path to green is long, TDD beginners often soldier on, writing an entire algorithm just to get one test to pass. This is highly risky, and also highly stressful. It is also not TDD.
Let’s look at an example. Suppose you have these tests (Java / JUnit):
And suppose you have been writing the simplest possible thing that works, so your code looks something like this:
Now imagine you picked this as the next test:
This is a huge leap from the current algorithm, as any attempt to code it up will demonstrate. Have a go yourself right now; go ahead, I’ll wait…
I think there are probably two fundamental reasons why the next test might take a lot of work to get passing: Firstly, the test may be genuinely a long way from the current functionality. In this case, maybe it’s worth looking for a smaller slice of the product to tackle.
Or secondly, there is probably a lot of coupling between the code and the existing tests. This is the situation in our example above, and our first priority should be to address that coupling.
So, what did you find when you tried to get the test above to GREEN? The simplest I could do was to write pretty much the whole algorithm. Why? Well, the code duplicates the tests at this point ("happy"
occurs as a fixed string in several places), so we probably forgot the REFACTOR step! It is time to remove the duplication before proceeding.
Some TDDers would use the triangulation technique here by writing a new test that fails precisely because of the coupling in the existing code:
YMMV, but personally I find this failing test to be a distraction. I prefer to focus purely on the coupling and let that take my code to a better place. So, without writing any new tests, what should I refactor to fix that coupling?
Everything revolves around those seven instances of “happy”
. So looking at this from the point of view of implicit coupling, I note that line 7 in the code will break if the test passes anything other than “happy”
. But in that case the happy
in my result string should simply be the same as the input text
. So I can change the code to be this:
Everything is still GREEN, and we now have only six instances of “happy”
.
But the large step we had to take for the “happy monday”
test was caused by line 5 in the code, so let’s look at that now. Let’s change it to this:
This passes the tests, but it is still implicitly coupled to the test code because of that 2
. That’s simple to fix too:
Now only the tests mention “happy”
, so I’m happy too!
Now let’s add back our difficult test:
I can make this pass more easily now:
It ain’t pretty, but it passes the tests. And now I have some obvious coupling to attack, which will help me get closer to a complete working implementation.
I think the key point for me here is that by addressing the coupling during the REFACTOR step we are making the next RED-GREEN cycle easier. Refactoring on GREEN is much less stressful than trying to hack in a big change on RED, so that’s where I want to focus my energy.
One of the reasons this newsletter exists is that I want to explore the effects of addressing implicit coupling. So far, based on very few examples, it seems to me that most of the time (citation needed) converting implicit coupling into explicit coupling also adds some generality to the code. That’s what happened in our example above. And it seems to me that such generality usually helps to make it easier to add the next piece of functionality. Let’s find out together if those ideas hold…
The step from red bar to green bar should be fast. If it isn't, you're writing code that is unlikely to be 100% tested, and which is prone to errors. Choose tests so that the steps are small, and make sure to refactor ALL of the duplication away before writing the next test, so that you don't have to code around it whilst at the same time trying to get to green.
Things to try
If you notice that you need to write or change more than 3-4 lines of code while your tests are RED, stop! Revert back to GREEN. Now either refactor your code in the light of what just happened, so as to make that test easier to pass, or pick a different test that is closer to your current behaviour.
Review your existing tests — how coupled are they to your code?
I gave it a try, my "quick" solution was 15 lines and despite thinking I have it and tests will pass, i ran tests twice during that and they failed, only on the 3rd try it was passing. Were just minor tweaks that were necessary like adding a space between the words but still, unexpected test failures.
In case this comment understands markdown, and someone is interested to look at my solution:
```java
if (text.contains(" ")) {
String[] strings = text.split(" ");
Map<String, Integer> map = new HashMap<>();
for (String string : strings) {
if (map.containsKey(string)) {
Integer integer = map.get(string);
map.put(string, integer + 1);
} else {
map.put(string, 1);
}
}
String result = "";
for (Entry<String, Integer> entry : map.entrySet()) {
result += entry.getKey() + "=" + entry.getValue() + " ";
}
return result.trim();
}
```
and now refactored using streams:
```java
if (text.contains(" ")) {
return Arrays.stream(text.split(" "))
.collect(Collectors.groupingBy(Function.identity(), Collectors.counting()))
.entrySet().stream()
.map(c -> c.getKey() + "=" + c.getValue())
.collect(Collectors.joining(" "));
}
```