In this post we’ll start with a simple MVVM codebase that uses a Service and make it more robust, then leverage that robustness for testability, and provide some alternative patterns for the pitfalls we find along the way.



This is the first part of a two-part blogpost. Follow this link if you were looking for the second part.

If you’re a visual learner I also provide some presentation slides here.

If you just want to fiddle around with the code, you can find it here.

And now, on with the show 🎩


# A simple MVVM codebase with a Service

The simplest there could be:

  • a Model that stores a boolean property
  • a Service that can toggle a boolean, accessed through a singleton instance
  • a ViewModel that holds a Model instance as well as a service instance
  • a View that defers business logic to a ViewModel
struct Model {
    var boolProperty: Bool
}

class Service {

    // We usually call this `shared` or `default`, this is just to be explicit.
    static let singleton = Service()

    func toggle(bool: Bool) -> Bool {
        !bool
    }
}

struct ViewModel {

    private(set) var model: Model
    let service: Service = .singleton

    init(model: Model) {
        self.model = model
    }

    func getUpdatedValue() -> Bool {
        service.toggle(bool: model.boolProperty)
    }
}

class View: UIView {

    private(set) var viewModel: ViewModel

    required init(viewModel: ViewModel) {
        self.viewModel = viewModel

        setupViewVisibility()
    }

    required init?(coder: NSCoder) {
        fatalError("init(coder:) has not been implemented")
    }

    func setupViewVisibility() {
        isHidden = viewModel.getUpdatedValue()
    }
}

If you’re looking at this example and wondering “What could be wrong with it?”, fret not. It’s not wrong! In fact, this is about as good as most codebases could hope to become. But that doesn’t mean we can’t make it better. 🦾✨

First things first, let’s learn what good code™ should aspire to be. 🧑‍🎓

# SOLID Code

There are 5 widely used principles to structure our code and increase maintainability:

  • Single responsibility
  • Open–closed
  • Liskov substitution
  • Interface segregation
  • Dependency inversion

Today we’ll focus on the Dependency Inversion Principle. It follows a couple of simple rules:

High-level modules should not import anything from low-level modules.
Both should depend on abstractions.

Abstractions should not depend on details.
Details should depend on abstractions.

One way of interpreting this in Swift terms we’re familiar with:

Implementations should depend on Protocols.

If we have an object (class Object) that is necessary for some piece of code to work (func doStuff(with object: Object)), we should replace it with a protocol representing it (protocol ObjectRepresentable), and have the object conform to that protocol (class Object: ObjectRepresentable). That way, we can more easily inject (foreshadowing) a substitute in its place, without worrying about the implementation details of the original object (func doStuff(with object: ObjectRepresentable)). In code, it means going from this

// Concrete implementation
class ObjectImplementation {
  /* implementation details */
}

// Code that relies our implementation
func doStuff(with object: Object) {
   /* do stuff */
}

// Using the code with an instance of the concrete implementation
let objectInstance: ObjectImplementation = .init()
doStuff(with object: objectInstance)

to this

// Abstraction
protocol ObjectRepresentable {
  /* implementation blueprint */
}

// Concrete implementation
class ObjectImplementation: ObjectRepresentable {
  /* implementation details */
}

// Code that relies our abstraction
func doStuff(with object: ObjectRepresentable) {
   /* do stuff */
}

// Using the code with an instance of the concrete implementation
let objectInstance: ObjectImplementation = .init()
doStuff(with object: objectInstance)

by inverting the exposed dependencies from expecting concrete implementations to expecting abstractions.

This doesn’t seem like it can yield any benefits to our working code, and honestly, it might not. But what it does yield is the ability to now test our code more easily using Dependency Injection (DI).

# Dependency Injection

Let’s put what we just learned about Dependency Injection into practice using our simple MVVM codebase from early on.

# Model

struct Model {
    var boolProperty: Bool
}

Oh wow, nothing to do here. This piece of code is as amazing as can be 🥳 You’re doing great!

# Service

Looking at our earlier example of a simple service, a couple of things should now standout:

  • It relies on a singleton that doesn’t conform to any protocol 😱
  • It cannot be reused and repurposed for testing easily. 🔧 (Don’t just take my word for it, try it yourself.)
class Service {

    static let singleton = Service()

    func toggle(bool: Bool) -> Bool {
        !bool
    }
}

But luckily, we now know how to fix both of those: conforming to an abstraction!

  • Make our class conform to a protocol.
  • Tip 🧠: The simplest way to get started on a protocol is to just list the existing method and property signatures, stripping them of implementation details.
protocol ServiceProtocol { // 👈
    func toggle(bool: Bool) -> Bool
}

class ServiceImplementation: ServiceProtocol { // 👈

    static let singleton = ServiceImplementation()

    func toggle(bool: Bool) -> Bool {
        !bool
    }
}

# ViewModel

What about the ViewModel (VM)?

struct NaiveViewModel {

    private(set) var model: Model
    let service: ServiceImplementation = .singleton

    init(model: Model) {
        self.model = model
    }

    func getUpdatedValue() -> Bool {
        service.toggle(bool: model.boolProperty)
    }
}

Our initial naive implementation uses a hardcoded dependency 🔨 for the Service, so let’s start by fixing that. We could inject this dependency through a property:

struct MehDiViewModel {

    private(set) var model: Model
    var service: ServiceImplementation? // 👈

    init(model: Model) {
        self.model = model
    }

    func getUpdatedValue() -> Bool {
        service?.toggle(bool: model.boolProperty) ?? true // 👈
    }
}

// ...

let viewModel = MehDiViewModel(model: .init(booleanProperty: true))
viewModel.service = ServiceImplementation.singleton

But we are still a few issues with the VM:

  • it expects a concrete dependency implementation. 😞
  • it expects the dependency to be injected at some point 🔮, otherwise it won’t function properly.
  • it forces us to use a default value, which is a business logic decision ⚠️

Let’s fix those as well 💪

struct BetterDiViewModel {

    private(set) var model: Model
    let service: ServiceProtocol // 👈

    init(
        model: Model,
        service: ServiceProtocol = ServiceImplementation.singleton // 👈
    ) {
        self.model = model
        self.service = service
    }

    func getUpdatedValue() -> Bool {
        service.toggle(bool: model.boolProperty) // 👈
    }
}

Now the VM’s dependencies are injected through the initializer, and they provide a default implementation. Moreover, we now specify that the service conforms to an abstraction. 🙌

Can we do any better? 🤔

Just one more tiny change to go 🤏 Making the VM conform to an abstraction as well! We’ve done this for the Service before, the reasoning is pretty much the same: just use the existing method and property signatures, and strip them of any implementation details.

protocol DiViewModelRepresentable { // 👈

    var model: Model { get }
    var service: ServiceProtocol { get }

    func getUpdatedValue() -> Bool
}

struct DiViewModel: DiViewModelRepresentable { // 👈

    private(set) var model: Model
    let service: ServiceProtocol

    init(
        model: Model,
        service: ServiceProtocol = ServiceImplementation.singleton
    ) {
        self.model = model
        self.service = service
    }

    func getUpdatedValue() -> Bool {
        service.toggle(bool: model.boolProperty)
    }
}

Now that the VM is bound by a protocol, we have enabled it to easily be mocked for testing purposes. 🐛

# DI and Unit Tests

If we’ve done everything right up until now, we won’t have changed the behaviour of our simple MVVM codebase. But can we be sure? It’s time to write some unit tests and make it official!

Testing out ServiceImplementation is easy enough.

final class ServiceTests: XCTestCase {

    func testService_TogglesBooleans() throws {

        let sut: ServiceImplementation = .init()
        XCTAssertTrue(sut.toggle(bool: false))
        XCTAssertFalse(sut.toggle(bool: true))
    }
}

# ViewModel

Before we test our DIViewModel, let’s first create a mock class that we can inject into its dependency: a MockServiceImplementation that also conforms to the Service protocol our view model will need.

Our first instinct might be to create an instance for every desired outcome we want to test out. We call these hardcoded-behaviour instances stubs.

class NaiveStubServiceImplementation: ServiceProtocol {

    func toggle(bool: Bool) -> Bool {
        true
    }
}

class NaiveStub2ServiceImplementation: ServiceProtocol {

    func toggle(bool: Bool) -> Bool {
        false
    }
}

They can be right tool ⛏️, but more often than not they don’t allow us to mock the inner workings without replicating the entirety of the implementation. This can quickly result in large amounts of duplicated code just to specify minute changes 😞

Let’s do better, let’s create a proper mock of the ServiceImplementation class 🧠

class MockServiceImplementation: ServiceProtocol {

    var toggleClosure: ((Bool) -> Bool)! // 👈
    func toggle(bool: Bool) -> Bool {
        toggleClosure(bool)
    }
}

We have a simple but accessible way of customizing the behaviour, without having to define instances entirely. 🙌 Don’t mind the forced-unwrapping ❗ that is taking place, as it will only be used in the testing environment and we want things to break when they don’t meet our expectations!

Now we can look at those tests for our ViewModel

final class ViewModelTests: XCTestCase {

    func testViewModel_WithTrueModelAndDefaultService_TogglesBoolean() throws {

        let model: Model = .init(boolProperty: true)
        // SUT - Subject Under Test
        let sut: DiViewModel = .init(
            model: model
        )

        XCTAssertFalse(sut.getUpdatedValue())
    }

    func testViewModel_WithFalseModelAndDefaultService_TogglesBoolean() throws {

        let model: Model = .init(boolProperty: false)
        let sut: DiViewModel = .init(
            model: model
        )

        XCTAssertTrue(sut.getUpdatedValue())
    }

    // MARK: Service Stubs
    func testViewModel_WithServiceStubSetToTrue_ReturnsTrue() throws {

        let model: Model = .init(boolProperty: true)
        let service: MockServiceImplementation = .init()
        let sut: DiViewModel = .init(
            model: model,
            service: service
        )

        let serviceToggleExpectation = expectation(description: "serviceToggleExpectation")
        service.toggleClosure = { _ in
            serviceToggleExpectation.fulfill()
            return true
        }

        XCTAssertTrue(sut.getUpdatedValue())

        wait(for: [serviceToggleExpectation], timeout: 0.1)
    }

    func testViewModel_WithServiceStubSetToFalse_ReturnsTrue() throws {

        let model: Model = .init(boolProperty: true)
        let service: MockServiceImplementation = .init()
        let sut: DiViewModel = .init(
            model: model,
            service: service
        )

        let serviceToggleExpectation = expectation(description: "serviceToggleExpectation")
        service.toggleClosure = { _ in
            serviceToggleExpectation.fulfill()
            return false
        }

        XCTAssertFalse(sut.getUpdatedValue())

        wait(for: [serviceToggleExpectation], timeout: 0.1)
    }
}

Hopefully this blogpost helped you improve the testability of your codebase. 🤞

But are we done? What are the pitfalls of our DI implementations? 🤔 Be sure to check out part 2 for the answer!




# Sources

Definitions: