Dependency Injection in Swift

Dependency Injection in Swift

ยท

5 min read

As a good developer, we all want to write software with as few bugs as possible. In order to achieve the same, we add unit tests, integration tests, practice TDD (Test-Driven Development), BDD (Behaviour-Driven Development) and we keep exploring opportunities to strengthen our quality gates. We want to catch bugs before it goes to production. In this article, I'm going to share the knowledge that I gained over recent days about writing testable code. If you are wondering "Why do I want to write unit tests?", or still thinking that "Unit tests are going to affect my productivity"? I would recommend you read my previous article and then read this one.

Once we realize the benefits of writing unit tests, our developer's instinct will urge us to write tests for the codebase that we are currently working on. And most often that is not that easy to add tests for the existing codebase.

We get STUCK ๐Ÿคฏ!

It could be because of the below reasons,

  • Your view controller is massive and not sure where to start
  • Your view controller or view model class is tightly coupled with other components in the application
  • Networking requests are scattered all around the codebase and it is very difficult to mock them

In simple words - "Your code is not testable". So now we need to make our existing codebase testable and any new code added should be designed carefully by keeping testability in mind. In my opinion, this is a crucial part of designing the software.

Testability:

Testability is simply the ability to isolate your code from dependencies to test. There are two popular ways to make your code testable.

Pure Functions:

A function is considered "pure" when it doesn't generate any side-effects, so that we always get the exact same output for a given input, no matter where or how many times the function is called.

Writing pure functions wherever possible is a recommended practice and they are testable directly without any mocks.

func calculateTotalPrice(product: Product, quantity: Int) -> Int {
      return product.price * quantity
}

If you see this pure function, it doesn't create any side-effects. So it is very easy and straightforward to test. You can create a product object and pass it along with this function and assert the result.

func test_calculateTotalPrice() {
      let product = Product(id: 1, price: 10)
      let totalPrice = sut.calculateTotalPrice(product: product, quantity: 5)
      XCTAssertEqual(totalPrice, 50, message: "calculateTotalPrice produced wrong total price")
}

Dependency Injection:

If your SUT(System Under Test) has no dependencies, it is straightforward that you can test it directly without any need for injection. But when your SUT has dependencies, you need to isolate it by injecting them. I couldn't think of a better definition of Dependency injection than the one James Shore defined.

Dependency injection means giving an object its instance variables. Really. That's it. - James Shore (Author of "The Art of Agile Development")

Dependency injection makes it very easy to replace an object's dependencies with mock objects, making unit tests easier to set up and isolate behavior.

In the below example, The class ArticleListViewModel has the dependency DatabaseManager.

class ArticleListViewModel {
    let databaseManager: DatabaseManager = ArticleDatabaseManager()

    func loadData() {
        databaseManager.fetchArticles { (articles) in
            //presentation logic
        }
    }
}

To write unit tests for this class, we should inject databaseManager in order to isolate the SUT. In our case, the SUT is ArticleViewModel. DatabaseManager is a protocol and ArticleDatabaseManager has conformed to the protocol DatabaseManager. We can inject databaseManagerby any reference that conforms to the protocol DatabaseManager.

There are different ways of injecting dependencies to your SUT,

  • Constructor Injection
  • Property Injection
  • Method Injection
  • Extract and Override Call

Constructor Injection: (Initializer Injection)

Constructor injection is a way to inject your dependencies using the constructor function. This makes your dependencies very explicit.

class ArticleListViewModel {
    let databaseManager: DatabaseManager

    init(databaseManager: DatabaseManager = ArticleDatabaseManager()) {
        self.databaseManager = databaseManager
    }

    func loadData() {
        databaseManager.fetchArticles { (articles) in
            //presentation logic
        }
    }
}

let viewModel = ArticleListViewModel(databaseManager: MockDatabaseManager())

The beauty of the Swift language is that you can have a default value for the arguments which makes it easier to inject dependencies without impacting the callers. Well, there is a disadvantage to this approach. This approach is not so good if you have to inject a long list of dependencies through the initializer function and that's a code smell. Having a long list of dependencies is actually violating the Single Responsibility Principle.

Property Injection:

In Property Injection, we inject the dependency after initialization through setters.

class ArticleListViewModel {
    var databaseManager: DatabaseManager?

    func loadData() {
        databaseManager?.fetchArticles { (articles) in
            //presentation logic
        }
    }
}

let viewModel = ArticleListViewModel()
viewModel.databaseManager = MockDatabaseManager()

In this approach, there is a possibility of an incomplete initialization. So this method is best when there is a specific default value to the dependency.

Method Injection:

We can inject dependencies through function parameters if they're referenced only within that function.

class ArticleListViewModel {
    func loadData(databaseManager: DatabaseManager = ArticleDatabaseManager()) {
        databaseManager.fetchArticles { (articles) in
            //presentation logic
        }
    }
}

let viewModel = ArticleListViewModel()
viewModel.loadData(databaseManager: MockDatabaseManager())

This technique is good when you have to inject the app-specific context. For example, we can pass a parameter currentDate with a default value Date() whereas in our test cases, it will be easy for us to mock this parameter with the known fixed date value.

Extract and Override Call:

This technique should be our last resort when we are not at all able to inject our dependencies using the above three methods. This is not exactly the Dependency Injection technique as we are not going to inject the dependencies directly to our SUT. Rather, we will create a subclass for SUT and override the dependency with a mock object.

class ArticleListViewModel {
    var databaseManager: DatabaseManager {
        return ArticleDatabaseManager()
    }

    func loadData() {
        databaseManager.fetchArticles { (articles) in
            //presentation logic
        }
    }
}

class MockArticleListViewModel: ArticleListViewModel {
    override var databaseManager: DatabaseManager {
        return MockDatabaseManager()
    }
}

The MockArticleListViewModel is a subclass of our original SUT ArticleListViewModel. Now we will be using MockArticleListViewModel as our SUT in our tests. This technique is effective with legacy codes.

We're all set to inject dependencies and continue with our unit testing journey!

injection.gif

I hope this article is useful. Thank you for reading this! ๐Ÿ™

Happy Coding! Happy Testing!

objc.io/issues/15-testing/dependency-inject.. jamesshore.com/v2/blog/2006/dependency-inje.. developer.apple.com/documentation/xcode/add..