How getting rid of singletons boosts testability


This is the fourth post in my series on MVVM with ReactiveCocoa 3/4 in Swift.

The problem with singletons

In the past, I used to write a lot of code like this to retrieve my model objects:

let matches = Store.sharedInstance().fetchMatches()

(Of course, Store could also be called differently. Common choices are APIClient, NetworkManager or, in this case, MatchStore.)

At some point I learned about reactive programming and realized that fetchMatches() should probably be made asynchronous. It should also starts its work whenever someone is interested in the result. ReactiveCocoa 3 offers signal producers to solve this problem:

let matchesSignalProducer = Store.sharedInstance().fetchMatches()

matchesSignalProducer.start(next: { [weak self] matches in
    self?.matches = matches
})

In an MVVM app such as SwiftGoal, the above code would reside in a view model. One of the architecture’s “selling points” is that your app logic becomes more testable, because it resides in view models that define inputs and outputs. This is, after all, the essence of testing: Does the class yield the correct output for a given input?

Suppose we want to write this simple test:

  1. Make the view model active. (Input)
  2. Check that it yields the correct amount of matches. (Output)

It turns out that our Store being a singleton becomes quite a blocker here. How are we supposed to provide a known amount of matches to the view model for testing? Maybe we could set the number somehow from our testing code, but won’t that break tests we may have for other cases, e.g. when there aren’t any matches?

Enter dependency injection

As I described in an earlier post, we can clean up our app’s spaghetti-like dependency graph by passing references to instances that a class relies on, preferably during initialization. Swift is particularly nice here, because we can easily declare a designated initializer to make this requirement explicit:

// MatchesViewModel.swift

init(store: Store) {
    self.store = store
    // …
}

Now it’s perfectly clear for any consumer of this view model that it requires a Store instance to work with. In our test suite, we can simply subclass Store with a MockStore, override the fetchMatches() method, and return a known amount of matches that will allow us to write simple, but powerful tests like the one described above. We can even set a flag to check whether the method was called correctly.

class MockStore: Store {
    // …
    var didFetchMatches = false

    override fetchMatches() -> SignalProducer<[Match], NSError> {
        didFetchMatches = true
        return SignalProducer.value([match1, match2])
    }
}

class MatchesViewModelSpec: QuickSpec {
    override func spec() {
        it("fetches a list of matches after becoming active") {
            let mockStore = MockStore()
            let matchesViewModel = MatchesViewModel(store: mockStore)
            matchesViewModel.active = true

            expect(mockStore.didFetchMatches).toEventually(beTrue())
            expect(matchesViewModel.numberOfMatches()).toEventually(equal(2))
        }
    }
}

Growing confidence

There is another huge benefit to this approach, which is that you can’t get your app into an inconsistent state quite so easily. Our Store has a base URL, which it uses to construct its network requests, and requires this at – you guessed it – initialization. In SwiftGoal, there is actually a feature that lets the user modify the base URL in the app’s settings at any time to connect to a different server.

Imagine the mayhem that would ensue if we had a singleton store and changed that base URL under everyone’s nose! Your user might have a new “Create Match” dialog open, and hitting “Save” would create the match on a different server than was used to select the players in the first place. Quite a recipe for broken app state :grimacing:

Instead, the app delegate (which has good enough reason to be a singleton) notices when the setting changes, and initializes a whole new hierarchy with a fresh store and view models that depend on it. This top-down approach allows the view models to be confident that their store’s base URL doesn’t change suddenly and unnoticedly.