Living in the test zone

Spread the love

I have previously written that, when given the option of many similar implementation strategies, you should prefer the one which is easiest to test. There is a simple corollary to this proposition: whenever possible, grow your codebase in areas covered by tests. In other words, your features should be living in the test zone.

To explore this guideline, let’s consider a simple application model. We have an entry point where the main logic loop is initialized and kicked off. In parallel, we initialize another background operation that needs to do some housekeeping (e.g. perhaps we need to clean up some temporary files generated by the main loop). If any one of these tasks fail, we will fail the whole program (presumably this program is some sort of daemon which will be restarted on failure).

public class EntryPoint
{
    // . . .

    public async Task RunAsync(CancellationToken token)
    {
        MainLoop loop1 = InitMain();
        Task task1 = loop1.RunAsync(token);

        HousekeepingLoop loop2 = InitHousekeeping();
        Task task2 = other.RunAsync(token);

        Task any = await Task.WhenAny(task1, task2);
        await any;
    }

    private MainLoop InitMain()
    {
        var config = ReadConfig("Main");
        // . . . lots of external/integration code here to
        // inject dependencies for the main loop . . .
        MainLoop loop = new MainLoop(/* . . . */);
        loop.DelayInterval = config.DelayInterval;
        // . . . set configured values . . .
        return loop;
    }

    private HousekeepingLoop InitHousekeeping()
    {
        var config = ReadConfig("Housekeeping");
        // . . . lots of external/integration code here to
        // inject dependencies for the housekeeping loop . . .
        Housekeeping loop = new HousekeepingLoop(/* . . . */);
        loop.DelayInterval = config.DelayInterval;
        // . . . set other configured values . . .
        return loop;
    }

    // . . .
}

As the comments indicate, the elided code deals with configuration as well as a lot of external dependencies which would not be suitable for unit testing. We would thus say that the entry point is not testable. This is okay — as a faithful entry point, it is mainly there to do wire up and should not contain a lot of fancy logic that has any appreciable cyclomatic complexity.

Now consider a change request for this application; it turns out that the housekeeping loop sometimes runs into issues and the program is crashing too much. As a result, we would like a feature toggle to disable the housekeeping loop without having to recompile or redeploy the whole application. We have several choices for how to implement this feature, but ultimately there are two broad strategies:

  1. Approach from the outside (change the entry point)
  2. Approach from the inside (change the loop itself)

If we want to live in the test zone, we should pick the latter strategy. Adding code in the entry point to deal with feature toggles and letting that code go untested is asking for trouble. By changing the inner loop implementation, we can write several targeted tests to make sure the behavior matches the specification. We can even get bonus testability points if we can think of a way to implement the feature without modifying the entry point at all — and thus virtually eliminating the risk of bugs!

Here is one way to do just that. Observe that the HousekeepingLoop already exposes a DelayInterval configuration value. Having a negative delay would be meaningless, so we can take advantage of that fact by defining any negative value as “disabled.” Voila! We have extracted a feature toggle from the existing code. The implementation may be as simple as the following:

// Inside the HousekeepingLoop class . . .
public Task RunAsync(CancellationToken token)
{
    if (this.DelayInterval < TimeSpan.Zero)
    {
        // Sleep forever, do nothing until canceled!
        return Task.Delay(-1, token);
    }

    // otherwise, real logic starts here . . .
    return this.RealRunAsync(token);
}

Consider the changes you are making to your code on a regular basis. Are you sitting safely in the test zone?

Leave a Reply

Your email address will not be published. Required fields are marked *