The Testing Shunt

In my software development experience, I’ve worked on numerous products. Most of these products have been around for a long time, and have been modified or enhanced over the year by an ever-changing roster of software developers. A natural consequence of this is that knowledge is lost, the codebase is stamped with the fingerprints of different coding styles and practices, and as the days go by, it becomes harder and harder to make changes to the code without breaking things.

Lately, this dynamic has been salient in my current role, as I work with a legacy product that has been subjected to the natural shifting forces of software development over the years. The product is complex, brittle, and delicate; we often make changes that break other areas of the product that, at face value, seem completely unrelated. How can a program continue to soldier-on when the cost and risk of development have increased to an unmanageable point? Well… it can persist in part by slowly reversing the buildup of coding-plaque through the means of wrapping existing behavior in unit tests. As more and more unit tests are written, more and more behavior of the program are validated, allowing us as developers to make changes with greater confidence that we’re not going to break anything.

On of the techniques that I have found to be invaluable with wrapping existing (object-oriented) code in unit tests is leveraging a Test Shunt.

A Test Shunt is a class that derives from the class to be tested, and overrides a specific part of class behavior to drive how the code behaves during the test. I have found shunts to especially useful when having to control behavior of a dependency within the tested class. Let’s say we have a legacy class with a buried dependency like below

import BuriedLibrary

class LegacyClass {

    func doLegacyWork() -> Any {
        if BuriedLibrary.shouldWorkBeDone() == true {
            // do the legacy work
        } else {
            // do something else
        }
    }
}

In this class, a library is used to get data, and our function then uses that data to conditionally determine what to do. Let’s also say that we need to make changes to our doLegacyWork function, and therefore we want to wrap our LegacyClass in unit tests. The unit tests we write should exercise all code paths of the doLegacyWork function. In this case, there are two paths that existing within the function. The first is when the conditional if statement returns true, and the second is when it returns false. So, how do we ensure that the tests that we write will exercise both of these paths? We currently do not have control over the return value of the BuriedLibrary.shouldWorkBeDone() function. Since we don’t have this control (in its current state), we cannot successfully, reliably, test the code paths. This is where the test shunt comes in. With a small refactoring, we can then use a test shunt to control the flow of logic.

The first step is to isolate the dependency on the buried library into its own function, like below.

import BuriedLibrary

class LegacyClass {

    func doLegacyWork() -> Any {
        if self.shouldWorkBeDone() == true {
            // do the legacy work
        } else {
            // do something else
        }
    }

    func shouldWorkBeDone() -> Bool {
        return BuriedLibrary.shouldWorkBeDone()
    }
}

Here, we’ve moved the call to the buried library dependency in it’s own function, and we refactored the statement within the doLegacyWork function to now indirectly call the dependency. This refactoring is a safe change to the code, as we are only adding a layer of indirection to the same functional call. Although, this simple change now enables us to create a test shunt, and gives us complete control over the code paths that need to be tested. This shunt will derive from the legacy class, and override the refactored function.

class LegacyClassTestShunt : LegacyClass {
    var shouldWorkBeDoneReturnValue : Bool = true
    
    override func shouldWorkBeDone() -> Bool {
        return shouldWorkBeDoneReturnValue
    }
}

Voila, this is the shunt. What we have now is a class that, for the most part, matches the behavior of the class that we want to test. The only difference is the call to the dependency; however, we’re not trying to test the dependency itself, but how we use that dependency. Since this shunt is a derived class, it has access to call the doLegacyWork function. Through the magic of inheritance, when we call this function, it will leverage the shunt’s version of the shouldWorkBeDone function.

Now, we are ready to use the shunt for unit tests. We’ll need to create two unit tests: one for a true value in the conditional, and one for the false value. Instead of using the legacy class directly in our unit tests, we’ll use the derived shunt.

func testDoLegacyWorkDoesLegacyWork() {
    var legacyClass = LegacyClassTestShunt()

    //this will force doLegacyWork to use the true conditional path
    legacyClass.shouldWorkBeDoneReturnValue = true
    
    var legacyWorkResult = legacyClass.doLegacyWork()
    XCTAssert(legayWorkResult == /* expected value */)
}

func testDoLegacyWorkDoesSomethingElse() {
    var legacyClass = LegacyClassTestShunt()

    //this will force doLegacyWork to use the false conditional path
    legacyClass.shouldWorkBeDoneReturnValue = false
    
    var legacyWorkResult = legacyClass.doLegacyWork()
    XCTAssert(legayWorkResult == /* expected value */)
}

That was easy! (And that’s why I’ve been liking the shunt so much.) The shunt is a great way to break-out buried dependencies so that you can have complete control over testing paths. This is a technique that I have found to be quite useful, and it’s something to keep in my back pocket.