As developers, we’re encouraged to write testable code, but what does that actually mean? What makes our code testable? To answer this question, let’s take a step back and assess the basic function of any useful computer program.

In general, programs should do three things:

  1. Receive input
  2. Perform computations
  3. Produce output

No program is terribly useful without some kind of input. Some examples of input are the command line, a database, a message queue, a web service… You get the picture. While it’s possible for us to craft a program that does not depend on input at runtime, in general, the majority of applications we build are useless without user data. Notable exceptions are generators, such as a hard-coded seed generator, but even in this case, the need for input hasn’t been removed from the program; instead, the input has been hoisted into the program’s design (connection strings, type data, etc.).

Of course, all programs are pointless without the ability to record output. All the input in the world doesn’t matter if programs don’t produce something, and indeed, we would never deliberately run a program that is known to never produce a result. It might be the case that a program produces multiple forms of output, such as log files and database records. It’s useful to think of output in terms of streams: log generation, files, database records, etc. In the classical sense, I think most of us will think of streams as objects that manipulate byte arrays. While this is true at the lowest level, at higher levels of abstraction, a stream can be as simple as an Rx Observable, which emits a sequence of T.

I’ve covered input and output from my general specification of “what programs should do.” What’s left is the computation. In general, computation is the thing that we’re interested in testing. Input and output are ancillary details that are necessary for our program to be useful, but should be abstracted from our processing code. Let’s consider the following C# program, which receives integers from the user, computes their square, and writes the square to the console.

using System;

public static class Program {
    const int MaxNumber = int.MaxValue / 2;

    static void Main(string[] args) {
        Console.WriteLine("Press Ctrl + C to exit.");
        while (true) {
            Console.Write("Please enter an integer: ");
            var input = Console.ReadLine();
            if (int.TryParse(input, out var number)) {
                if (Math.Abs(number) <= MaxNumber) {
                    var square = number * number;
                    Console.WriteLine($"The square of {number} is {square}");
                }
                else {
                    Console.Error.WriteLine($"ERR02: Computing a square for"
                        + $" {number} would result in integer overflow.");
                }
            }
            else {
                Console.Error.WriteLine($"ERR01: Unknown input '{number}'");
            }
        }
    }
}

In this example, the Console API is providing us with a stream (of sorts). The code we want to test depends on two things:

  1. A string input
  2. Production of a number, or an error

The code above does not throw (or even deal with) exceptions. There are several ways I can think of for how we could re-factor this code:

  • Create an API with the same signature as the .NET int.TryParse API
  • Create an API that receives a string, returns an integer, or throws an exception
  • Create an API that receives a string, and returns a multi-valued result

I don’t like the first option, because we have more than one error condition. The first error is when the input string is not an integer, and the second is when squaring the number could result in an overflow. The second option is okay - it is extremely simple to document and craft - but I really don’t like writing try/catch blocks. For these reasons, I am personally fond of the third option, because it is easier to connect to things like the RX and TPL Dataflow APIs.

C# 7.1 delivered the inferred tuple element names feature for the ValueTuple struct. This feature enables us to return multi-valued results from a method without having to resort to things like ref or out parameters, and without adding additional classes to our library (including anonymous classes).

When designing an API, I prefer to take the absolute most functional route possible, because the API will be the most connectable with the least coupling to my other APIs. This translates directly to more reusable APIs, simplifies testing, and minimizes the number of required test cases.

Refactoring the critical part of the computation is pretty simple. I’ve done this based on my stated preference towards functional APIs:

const int MaxNumber = int.MaxValue / 2;

public static (int errorNo, int value) Square(string input) {
    if (int.TryParse(input, out var value)) {
        if (Math.Abs(value) <= MaxNumber) {
            return (0, value * value);
        }
        else {
            return (2, -1);
        }
    }
    else {
        return (1, -1);
    }
}

If you’re not familiar with the (int errorNo, int value) syntax, please visit the inferred tuple element names section.

By refactoring this code, we’ve made the code callable, which enables us to simulate all sorts of input. It also enables us to subject the API to additional sources of input more easily, such as lines of a file. If we made the first value of the tuple an enum, we could also create a strongly-typed value-handler. These might be defined as:

enum SquareResult {
    Success = 0,
    ErrorNotAnInteger,
    ErrorValueTooLarge
}

delegate void SquareResultHandler((SquareResult errorNo, int value) result);

For conciseness, we’re going to stick to the (int errorNo, int value) definition shown above.

With our refactorings, our Main method becomes:

public static void Main(string[] args) {
    Console.WriteLine("Press Ctrl + C to exit.");
    while (true) {
        Console.Write("Please enter an integer: ");
        var input = Console.ReadLine();
        var result = Square(input);
        switch (result.errorNo) {
            case 2:
                Console.Error.WriteLine($"ERR02: Squaring the number {input}"
                    + " would have resulted in overflow.");
                break;
            case 1:
                Console.Error.WriteLine($"ERR01: The input '{input}' is not an"
                    + " integer.");
                break;
            default:
                Console.WriteLine($"The square of {input} is {result.value}");
                break;
        }
    }
}

At first glance, you might think this looks even more convoluted and complex than the logic we replaced, and I might agree with you about that, but remember, the whole point of this exercise is to enable testing of the parse+square routine. This new API is extraordinarily easy to test, requiring the following test cases:

  • Valid positive numeric input: 2 -> (0, 4)
  • Valid negative input: -3 -> (0, 9)
  • Not a number: (1, -1)
  • Number too large: int.MaxValue + 1 -> (2, -1)

The first of these, using MSTestv2, looks like this:

[TestClass]
public class SquareMethodTests {
    [TestMethod]
    public void Square_Succeeds_For_A_Positive_Int_String_Test() {
        var expect = (0, 34 * 34);
        var result = Program.Square("34");
        // The ValueTuple struct performs value-equality comparisons, not
        // reference equality. Thus, we can safely compare the `expect`
        // variable to the `result` variable via `AreEqual`.
        Assert.AreEqual(expect, result);
    }
}

The benefit of this decoupling is that we can now very easily verify all these cases. If we determine at some point that this code is not behaving properly at runtime, we can fall back on the tests to help prove that the Square method is doing the right thing. If we discover an issue, we can also fairly easily add logging at the periphery of the call (pre- and post-call log data) - which I find to be more useful in the majority of cases anyway - to help understand what cases we’ve missed.

You might read all this and think that it’s great for the contrived example I’ve crafted, but we can apply the same technique to the majority of APIs. I/O does not have a place in the majority of processing functions (or at least, not directly). To combat this, we can wrap I/O into an interface, or some form of a facade that enables us to intercept the call, enabling us to verify that the I/O was requested, while never truly dispatching I/O calls. Doing this is particularly easy when you use something like an Rx Subject, a delegate, or the Action<T> delegate.

Even with the brief treatment I’ve applied to testing business logic above, I know that many readers will likely not have any experience designing or implementing a model such as the one described above (with facades or observables). In a future post, I’ll describe some of the strategies I’ve employed to help ensure my code is testable. All of the examples will be constructed using the Moq framework, which is extraordinarily popular for testing C# code.

I hope you’ve found this post helpful! If you’ve never implemented any of the things I’ve described in this post, I would recommend taking some time to go grab your favorite tool chain, bootstrap a project with some tests, and implement some of the things I’ve described. I’d give the same advice for any language features I’ve employed that you may be unfamiliar with.

Thanks for reading!

- Brian