The Four Ways To Bypass Static Cling

by Dan Cutting

How can we test code that calls static functions in other classes?

Let’s say we have some legacy code. We’ll use Swift1 here, but any OO language will be comparable:

class TaxedItemPricer {

  let TaxRate = 1.2

  func totalPriceForItem(itemId: Int) -> Double {
    let price = ItemDAO.priceForItem(itemId);
    return price * TaxRate
  }
}

class ItemDAO {

  class func priceForItem(itemId: Int) -> Double {
    // Connect to a database and query the price.
  }
}

Writing this code was pretty convenient for the original author. The code just reaches out to a database to grab the raw price for the given item then multiplies it by the hard-coded tax rate.

We want to test that the total price given for an item is appropriately adjusted for tax. The unit test we’d like to write would look something like this:

class TaxedItemPricerTests: XCTestCase {

  func testTotalPriceForItem() {
    let subjectUnderTest = TaxedItemPricer()

    let itemId = 1
    let actualTotal = subjectUnderTest.totalPriceForItem(itemId)

    let expectedTotal = 120.0
    XCTAssertEqualWithAccuracy(expectedTotal, actualTotal, 0.01)
  }
}

But since the prices are coming out of a database, how can we set the price for item 1 to a known value? (It would need to be 100 in this test.)

This problem is sometimes known as static cling. Calling static functions often seems convenient, but it makes code harder to unit test because there’s no way for us to inject our test code at run time.

So how can we proceed?

Test Database

We could set up a test database with a known price for item 1 that matches up to our test. But this has some problems:

  • it can be a lot of work
  • tests will need to hit a real database every time they run which can be slow and may require a network connection
  • we’ll need to find the place in the code that connects to the database and modify it during our test runs to connect to our test database (with a preprocessor directive or runtime flag?)
  • if our test alters the database in any way, we need to be careful that tests are run in the correct order and the database is always in a known state

Using a real database here would make this more of an integration test than a unit test since it is testing not just our code, but also the interaction between two systems.

While these sorts of tests can be valuable, it’s not really what we’re after here. Unit tests need to run frequently during development, and as part of automated continuous integration builds. They should be quick and independent of external systems or network connections.

Refactor for Dependency Injection

An alternative is to fake the database dependency. To do this, we could make the TaxedItemPricer use a fake ItemDAO. Unfortunately, there’s no obvious way for us to intercept and fake this function call since it’s static.

If this code had been written using TDD, the ItemDAO dependency would have been injected and we could supply a fake that responded with a known value instead of connecting to a database.

So let’s refactor this code to accept an injected ItemDAO:

class TaxedItemPricer {
    
    let TaxRate = 1.2
    let itemDAO: ItemDAO
    
    init(itemDAO: ItemDAO) {
        self.itemDAO = itemDAO
    }
    
    func totalPriceForItem(itemId: Int) -> Double {
        let price = self.itemDAO.priceForItem(itemId);
        return price * TaxRate
    }
}

There are two catches to this refactor:

  • we need to fix all the places that construct a TaxedItemPricer to inject an ItemDAO
  • more significantly, we also need to make the priceForItem function of the ItemDAO class non-static which may cause significant ripples around the code base

Let’s assume we’ve accepted these shortcomings and made the necessary accommodations.

Now we can test our TaxedItemPricer by injecting a stub ItemDAO. The best practices for stubbing and mocking in Swift are still being figured out, but for now we can use the technique described by Andrew Bancroft.

We declare an inline subclass of the class we want to stub and make it return whatever canned values are appropriate for our test. Then we inject an instance of our stub class into the subject under test.

Let’s change our test to do this.

class TaxedItemPricerTests: XCTestCase {
    
    func testTotalPriceForItem() {

        class StubItemDAO: ItemDAO {
            
            override func priceForItem(itemId: Int) -> Double {
                let stubPrice = 100.0
                return stubPrice
            }
        }

        let stubItemDAO = StubItemDAO()
        let subjectUnderTest = TaxedItemPricer(itemDAO: stubItemDAO)

        let actualTotal = subjectUnderTest.totalPriceForItem(1)
        
        let expectedTotal = 120.0
        XCTAssertEqualWithAccuracy(expectedTotal, actualTotal, 0.01)
    }
}

Success! We can now inject a known price and make sure that the expected tax rate is added to it. Additionally, we’re not hitting any real databases, so our tests are fast and repeatable.

But this test has come at quite a cost. We’ve had to make significant changes to not only the class we are testing, but also the DAO that it uses, as well as all other parts of the code base that touch these two classes. And since none of this code was under test when we made the changes, it’s hard to be sure we haven’t broken something somewhere else.

Can we write a similar test without changing so much code?

Minimally Invasive Refactor

There is another way, and it’s one of my favourite techniques for testing legacy code.

The trick is a twist on the stubbing technique we just saw. Instead of changing our subject class to accept an injected ItemDAO, we simply extract the line of code that calls the static function to its own function, then override it in a subclass in our test suite to return our stubbed value.

Let’s go through both steps.

Step 1: Extract the static function call

Take the line of code that calls the static function and extract it to its own function. With decent IDEs, this can usually be achieved with an automatic refactor (which is safer than a manual refactor). This is the only code change needed.

class TaxedItemPricer {
  
  let TaxRate = 1.2
  
  func totalPriceForItem(itemId: Int) -> Double {
    let price = rawPriceForItem(itemId)
    return price * TaxRate
  }
  
  func rawPriceForItem(itemId: Int) -> Double {
    return ItemDAO.priceForItem(itemId);
  }
}

Step 2: Subclass and override

In our test case, subclass the subject under test and override the function we just extracted. Then instantiate this subclass and use it as our test subject.

import XCTest

class TaxedItemPricerTests: XCTestCase {
  
  func testTotalPriceForItem() {
    
    class TestableTaxedItemPricer: TaxedItemPricer {
      
      override func rawPriceForItem(itemId: Int) -> Double {
        let stubPrice = 100.0
        return stubPrice
      }
    }
    
    let subjectUnderTest = TestableTaxedItemPricer()
    
    let itemId = 1
    let actualTotal = subjectUnderTest.totalPriceForItem(itemId)
    
    let expectedTotal = 120.0
    XCTAssertEqualWithAccuracy(expectedTotal, actualTotal, 0.01)
  }
}

That’s it! We’ve made virtually no changes to the original code, and the change we did make was small and safe. Importantly, there are no ripples affecting other code in the code base. Finally, the test is clear and succinct, and does not rely on connecting to a real database.

There are some drawbacks to this approach. If many static functions are used by the class, you will end up with a lot of wrapper functions. In these cases, proper dependency injection may be preferable.

So can you use dependency injection without creating ripples around the code base? Yes!

Ripple-free Dependency Injection

This is a variant of the full dependency injection refactor discussed above.

Injecting an ItemDAO made testing straightforward but it also caused a lot of ripples to the rest of the code base.

If we refactor the code for dependency injection as above, and throw in some convenience functions, we can obviate the ripples.

class TaxedItemPricer {
  
  let TaxRate = 1.2
  let itemDAO: ItemDAO
  
  convenience init() {
    self.init(itemDAO: ItemDAO())
  }
  
  init(itemDAO: ItemDAO) {
    self.itemDAO = itemDAO
  }
  
  func totalPriceForItem(itemId: Int) -> Double {
    let price = self.itemDAO.priceForItem(itemId);
    return price * TaxRate
  }
}

class ItemDAO {
  
  class func priceForItem(itemId: Int) -> Double {
    // Connect to a database and query the price.
  }

  func priceForItem(itemId: Int) -> Double {
    return ItemDAO.priceForItem(itemId)
  }
}

As before, this is a more significant change than our minimally invasive refactor, but it gives us similar benefits: no code outside these classes would need to change, and we can still inject our stubbed ItemDAO for testing purposes. This could also be a cleaner approach if our subject class calls many static functions, since we do not need to create wrapper functions for each call.

However, it does result in some odd-looking code. The convenience initialiser in the TaxedItemPricer is sensible enough, but the instance function of the ItemDAO now calls the corresponding class function. It would be confusing to other people who come across this code, so it might be considerate to leave a comment explaining that this is intended as a transition away from a static approach.

Conclusion

We’ve tackled the problem of testing code that calls out to static functions in four ways. Each has advantages and drawbacks and ultimately the decision of which technique to apply will come down to the code base we’re working in.

If we can easily refactor for dependency injection, that should probably be considered first. If, however, the code base is large, complex or mostly untested, we might prefer to err on the side of caution and perform a minimally invasive refactor. Once more of the code base is covered with tests, we can then be more confident about moving to a full dependency injection approach. ✍

  1. In his excellent book, Working Effectively with Legacy Code, Michael Feathers defines legacy code as code without tests. Even though Swift is a pretty new language, you can bet there’s heaps of legacy code being written with it right now!