Saturday, April 18, 2020

TDD Test Bulk Heads

I wanted to illustrate some of the ideas in my previous essay with some code examples, in an attempt to spell out more clearly what I think can, and perhaps even should, be going on.

So let's imagine that we are working on a Mars Rover exercise, and we want to start from a trivial behavior: if we don't send any instructions to the rover, then it will report back its initial position.

Such a use case could be spelled a lot of different ways.  But if we are free of the constraints of legacy code, then we might reasonably begin with a primitive obsessed representation of this trivial behavior; the simple reason being that such a representation is easy to type.

The fastest way I can see to get from RED to GREEN is to simply wire in the correct response.

At this point in most TDD demonstrations, a second test will be introduced, so that one can begin triangulating on a solution. Alternatively, one could begin removing duplication right away.

For my purposes here, it is convenient to attack this problem in a very specific way - which is to recognize that the representation of the answer is a separate concern form the calculation of the answer. I assert that this sort of "extract method" refactoring should occur to the programmer at some point during the exercise.

I'm going to accelerate that refactoring to appear at the beginning of the exercise, before the function becomes too cluttered. The motivation here is to keep the example clear of distractions; please forgive that I am willing to give up some verisimilitude to achieve that.

That means that we can produce an equivalent behavior (and therefore pass the test), with this implementation:

The naming is dreadful, of course, but we've taken a step forward in separating calculation from presentation.

If we happen to notice that we've committed this refactoring, then an interesting possibility appears; we can decompose our existing assertion into two distinct pieces.

This version, with the double assertion, doesn't improve our real circumstances at all - but it begins to pave the way to a third version, which does have some interest:

With this change, we're constraining our behavior exactly as before... except that the new arrangement is somewhat more resilient when the required behaviors change in the future. For example, if we needed to replace this somewhat ad hoc string representation with a JSON document, only one of the two tests -- that test which constrains the behavior of the formatting logic -- fails and needs to be rewritten/retired.

I tend to use the pronunciation "behaves like" when considering tests of this form; the test doesn't assert that rover.run calls extractedMethod, only that it returns the same thing you would get by calling that method with the assigned arguments. For instance, if we were to inline that particular call (leaving the extracted method in place), the tests would still correctly pass.

That sounds pretty good. Where's the catch?

As best I can tell, the main catch is that we have to be confident that the signature of the new method is stable. By expressing the intended behavior in this way, we're increasing the coupling between the tests and the implementation, making that part of the implementation harder to change than if it were a hidden detail.

No comments:

Post a Comment