Quick Tip: Test functions with DI

Testing your code's collaborators is really important but how do you test functions like UIImage(named:) or NSLocalizedString(_:tableName:bundle:value:comment:)?


What are we testing?

We are not interested in testing whether methods like UIImage(named:) actually work as that's Apple's job but we should verify that we invoke the functions with the correct arguments.

Take the following example

class Images {
    
    func jumpSprite(atIndex index: Int) -> UIImage? {
        return UIImage(named: "jump_\(String(format: "%03d", index))")
    }

}

The above is a simple call to UIImage(named:) with some simple logic to build the image name based on the passed index. The thing that we need to test here is the image name construction logic - we would like our tests to verify that if we call the function with the input of 1 that it will invoke UIImage(named: "jump_001").


Dependency Injection to the rescue

To create a seam that allows testing the collaborator we can make UIImage(named:) injectable. Our production code can continue to use UIImage(named:) but our tests can use a different function that allows us to capture and verify the input.

Start by making it injectable with a sensible default

class Images {
    
    var loadImage = UIImage.init(named:)
    
    func jumpSprite(atIndex index: Int) -> UIImage? {
        return loadImage(named: "jump_\(String(format: "%03d", index))")
    }
    
}

In the above we made a couple of changes

  • Added a new variable of type (named: String) -> UIImage? that holds our image loading function
  • Invoke loadImage(named:) instead of directly invoking UIImage(named:)

End by adding some tests

Now that we have done the scaffolding we can add tests that verify that the correct arguments are provided when loading images

class ImagesTests: XCTestCase {
    
    func testJumpSpriteIsInvokedWithTheCorrectArguments() {
        let images = Images()
        
        var captured: String?
        
        images.loadImage = { name in
            captured = name
            return nil
        }
        
        images.jumpSprite(atIndex: 321)
        
        XCTAssertEqual("jump_321", captured)
    }
    
}

Conclusion

The fact that functions are a first-class type in Swift means that it is super simple to use dependency injection to enable testing of functions. It's always important to test our code's collaborators to ensure that we are calling their contracts correctly and with the arguments that we expect to send.