Rick Mugridge presented a paper “Test Driven Development and the Scientific Method” at the 2003 Agile Development Conference. There he speaks of theory simplicity and draws a parallel between Occam’s razor and the (positive) pressure of TDD to “choose the design with the fewer components, all things being equal.” (The primary citation given for this comparison is an e-mail thread from Mike Champion.) Another way to approach this is as a testability heuristic: prefer an implementation that is easier to test.
Here is a simple example. Imagine a workflow system that uses a core abstraction based on actions. You might end up with classes like these (and associated tests, of course, elided here for brevity):
public abstract class WorkflowAction { // ... public async Task RunAsync(WorkflowData data) { // ... some common pre-work ... await this.RunCoreAsync(data); // ... some common post-work ... } protected abstract Task RunCoreAsync(WorkflowData data); } public abstract class SequentialAction : WorkflowAction { private readonly List<WorkflowAction> innerActions; public SequentialAction(params WorkflowAction[] innerActions) { this.innerActions = new List<WorkflowAction>(innerActions); } protected override async Task RunCoreAsync(WorkflowData data) { foreach (WorkflowAction innerAction in this.innerActions) { await this.innerAction.RunAsync(data); } } }
So far, so good — we can run a general purpose action, and compose several actions into sequences. Now let’s say we want to implement a new feature involving a scoped resource which would be used by one or more inner actions. We want the resource to be created and destroyed in an orderly fashion by an outer workflow action so that the inner actions (who are just resource consumers) don’t need to be involved. Given the objects already defined in the system, we could choose several implementation strategies for this new object which we’ll call ScopedResourceAction
:
- We could make
ScopedResourceAction
derive fromSequentialAction
since the typical use case involves one or more inner actions which access the resource. - We could make
ScopedResourceAction
derive from plain oldWorkflowAction
and use composition (“has-a”) with a SequentialAction (e.g. by passing it as a constructor parameter). - We could make
ScopedResourceAction
derive from plain oldWorkflowAction
and use composition with a plain oldWorkflowAction
.
Of those options, we should bias towards the last one. Sure, the typical use case would involve running many inner actions, but that’s really just a detail whose specificity actually makes the TDD practitioner’s life slightly harder. Using an approach which relies on the specifics of SequentialAction
would lead to, at minimum, some duplication of test effort to re-verify some of the aspects already covered in that class’ tests (e.g. make sure that we properly handle zero, one, or many inner actions, make sure we propagate exceptions from both the first action or any later action, and so on).
The example may seem contrived but I’ve encountered many flavors of this problem many times. To generalize a bit from my earlier post “A loop is a responsibility”, pay careful attention to the essential business logic and bias toward simplicity. The design which is easier to test will tend to be the better option.
Pingback: Living in the test zone – WriteAsync .NET