Ever since reading Alex Eagle’s article Change-Detector Tests Considered Harmful, whenever I write I a unit test, I ask myself, “is this a change detector?” It’s a useful reflex to have, but I think Eagle’s article wasn’t completely clear on what it means for a unit test to be a change detector. In this article, I’ll provide concrete examples delineating the difference between change detectors and good unit tests.
Unit Tests Test Value
- The purpose of a unit test is to test the value which the unit under test adds to the application.
- There is value in using an abstract parts combinator.
- Looking at Eagle’s unit test example, we don’t know if the
Processorclass is an abstract parts combinator.
An Abstract Whatinator?
“Combinator” is a math term (and a functional programming term). Its technical meaning is irrelevant for this discussion. In this article, think of it as a component which has subcomponents which are usable elsewhere.
Breaking Down the Example
Let’s rewrite Eagle’s unit test in Java. It would look something like this.
And here is a minimal implementation of the implied classes.
Per Atkinson, the reason that we can’t tell if the
testProcessing unit test is worthwhile is that we don’t know what this code is doing.
Atkinson provides two cases, one to demonstrate how the
testProcessing unit test would be a useless change detector, and one to demonstrate how it would be a good unit test.
Change Detector Case: Woeful Interest Calculator
The example Atkinson uses to demonstrate how the
testProcessing unit test would be a change detector is an interest calculator in which the calculation is split into two parts.
To make this concrete, here is a very basic implementation of said interest calculator which is structurally identical to the
Processor class above. (Ignore the fact that you’d never write an interest calculator this way, mutating some object via
Notice that, as Atkinson points out, if the interest calculation is wrong, this test still passes.
This implementation is so horrendous that the best you could likely do with unit testing it is to make sure that both halves of the calculation actually happened (and then test the implementations elsewhere), but that’s mostly irrelevant. The point is, given how
AccruedInterestCalculator works, the
testProcessing test tests no added value and breaks when the implementation changes. It is a change detector.
The Responsibilities of the Unit
As an aside, if
AccruedInterestCalculator were doing anything beyond what its subcomponents do (for example, applying an early withdrawal penalty rate after the other calculations), there would be something of substance to test.
This is what Atkinson means when he talks about the unit under test having a “more concrete responsibility within the problem domain”.
With our unfortunate implementation (which mutates values via
void methods), you’d have to test that penalty with a fake instead of a mock.
Good Unit Test Case: Event Handler
The example which I will use to demonstrate how the
testProcessing unit test would be a good unit test is a late payment event handler.
LatePaymentEventHandler is structurally identical to the original
Processor class, the
testProcessing unit test is actually useful for
LatePaymentEventHandler is a combinator: its subcomponents,
CreditCardCharger and a
ReceiptEmailer, are useful elsewhere, and its value is in the way it combines them. This is Atkinson’s second case.
The value-add here is that the
CreditCardCharger is used before the
ReceiptEmailer, presumably throwing an error if something goes wrong and preventing an email from being sent out if the customer wasn’t actually charged. In this case, the subcomponent order does matter and so testing the subcomponent order is worthwhile.
The structure of the unit under test isn’t enough to determine whether or not a unit test is a change detector. You have to understand the work it is doing and how that adds value to the application, and test that.