Convenient Dependency Injection in Swift

Ever since Swift 1.0 was released, I have been struggling with finding a way to introduce some proper dependency injection into my code. The previous methods that were popular in objective-C like Swizzling or property overloading no longer works or require far too many changes to your code to make them actually worth it.

Natasha Murashev posted some nice ideas about dependency injection through default property values in functions, you can read more about that here.

But there's one problem with this solution, the default values require that the concrete implementation be dragged in and linked with your testing target, and that might be something that you don't want to do.

Today I bring you a way to solve these issues: by using default property values, (convenience) initilizers and type extensions to introduce some really convenient dependency injection.

The Example§

Let's start with the dependant type:

public struct Farm {
    
    private let animals: [Animal]
    
    init(animals: [Animal]) {
        self.animals = animals
    }
    
    public func feedAnimals() -> [String] {
        return animals.map { $0.feed() }
    }
    
}

The Farm houses a bunch of generic animals, Animal is just a protocol for our dependencies - the actual farm animals. Here's the protocol itself:

public protocol Animal {
    
    func feed() -> String
    
}

Now let's add some concrete implementations of this protocol that we'll use in our production app:

public struct Cow: Animal {
    
    public func feed() -> String {
        return "Moooo!"
    }
    
}

public class Duck: Animal {
    
    public func feed() -> String {
        return "Quack!"
    }
    
}

We ended up with a fully testable Farm type which we can use in our application by passing the right set of dependencies:

Farm(animals: [Cow(), Duck()]))

But DI isn't really DI unless you don't have to care about what you're passing to the constructor in your actual use case.

This is where class extensions come in. With Swift we can extend classes with convenience initializers and structs with just regular initializers wherever we like! So to do that create a separate swift file to house your DI initializers (you need to keep this in a different file to include it in your application and exclude it form your testing target, so you don't include the whole dependency tree for the type you're testing). In this file we would have:

extension Farm {
    
    init() {
        self.init(animals: [Cow(), Duck()])
    }
    
}

Since this is a swift extension, we can have whatever we want here, even returning a singleton instance of the interface you want to use. To put it simply: you're only limited by Swift itself when it comes to deciding how you want your dependencies executed and your dependency tree handled. For the sake of this example I chose to use the most simple case.

On to the Tests!§

Now we want to unit test the Farm struct. We didn't import the source file that contains conveniences and the dependency tree, so we don't have to import types for Cow and Duck. Instead let's create a new mock type for our Animal protocol and execute the test with that:

struct TestAnimal: Animal {
    
    func feed() -> String {
        return "This isn't an actual animal, it's just a test"
    }
    
}

class InjectTests: XCTestCase {
    
    func testFeeding() {
        let farm = Farm(animals: [TestAnimal()])
        let result = farm.feedAnimals()
        
        XCTAssertEqual(result, ["This is not an actual animal, it's just a test"])
    }
    
}

And here you go, a fully contained and convenient dependency injection method, that works without the need of any framework, objective-c code, var's (instead of lets) or dynamism (instead of static dispatch).

Project Source§

~I've made a simple iOS project as a proof of concept for this: download~ the project is out of date and no longer hosted


Comments