-2

What is the meaning of:

You are not allowed to write any more of a unit test than is sufficient to fail; and compilation failures are failure

by Robert Martin on TDD? Is it write only the needed unit test and not more than that? What about "compilation failures are failures"?

4

TDD suggests a red–green–refactor cycle. This cycle works better the smaller it is. We should make a tiny change in each cycle, and have a test that shows that this change was necessary. The flip side of this is that the TDD tests should be created in really small steps as well, so that a tiny change is sufficient to make them pass.

For example, consider a function that turns a CSS color name like red into a hex code like #FF0000. We might be tempted to write this kind of test in TDD:

@Test
void canConvertColor() {
  assertEquals(hexFromName("red"), "#FF00000");
}

But we can take much smaller steps. First, just make sure that we have a function:

@Test
void canConvertColor() {
  hexFromName();
}

Oh no, the test doesn't compile! But this is OK: the test is failing, we are in the “red” part of the cycle. So we just add a stub void hexFromName() {} to make the test pass. Next cycle!

This function is supposed to take a String as argument. So update the test:

@Test
void canConvertColor() {
  hexFromName("red");
}

Again, the test fails to compile because we are providing an argument although the function expects none. So we update our stub to take an argument: void hexFromName(String name) {}. Now the test passes, and we can continue to the next cycle.

The function should return another string, so we update the test case to use the return value as in the first test case I've shown. The test will probably fail to compile again, so we fix our stub to have a return type String hexFromName(String name) { return ""; }. Ah, ok, the test still fails so the supposed change was a bit too large. Let's return "#FF0000" to make the test pass.

And so on.

So it's possible to take really tiny steps. And if the test stops compiling, that's sufficient to make the test “red” – we can write some code to make it pass again.

For a simple function like hexFromName(), taking these tiny steps is not necessarily useful. But doing an exercise like this can be helpful to think about our actual requirements while writing the tests, instead of making broad assumptions. It also provides more confidence along the way that our code is actually starting to provide more value.

That's especially useful when TDD-designing a larger class or interface, where our test starts to require a ton of behaviour to make it pass. When the red–green–refactor cycle takes very long, we don't have as much confidence that our changes are moving in the right direction. One of my worst “mistakes” was a TDD-cycle that ended up taking a whole week because I wanted to make a huge change in one go, but I was effectively flying blind the whole time. Splitting up the change into smaller cycles would have taken a bit more initial effort, but would have provided more visibility, confidence, and would likely have been faster.

Whether micro-TDD is feasible in practice depends a lot on your tooling: when building and running tests takes more than a few seconds, such tiny steps break the flow of programming. So I would recommend doing micro-TDD as an exercise, not necessarily as part of everyday work. On the other hand, tooling can be improved. Things like background compilation, test runners that run failed tests first, and an architecture that allows components to be tested independently have compounding effects on developer productivity because they help keep the cycle time as short as possible.

1

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.