Spy Stub Dummy Fake Mock in Swift

Swift Test Doubles Spy Stub Dummy Fake Mock



Unit testing is most effective when the unit tests are deterministic. And by deterministic means, the output is always the same for a specific input. No surprise, no randomness whatsoever. Not only in Swift but in all languages, test doubles like spy, stub, dummy, fake, and mock play crucial roles in making those tests deterministic. Without understanding the fundamental differences among them will create confusion in a team. In this blog post, we will be elaborating each test doubles with examples.

Usually, any component we want to unit test has one or more dependencies. To test an expected behavior, we need to fake some input somehow. Assume we need to store a user preference in some persistence store. For now, assume it will be User Defaults. We will use abstraction so the high-level business logic and low-level details do not depend on each other. Instead, they both depend on abstraction. Here, the logic to store the user preference is high-level logic. And the actual storing of User details is the low-level details.

The basics of dependency Inversion

We just mentioned both the higher-level logic and the low-level details should depend on abstraction. We have some details in our Introduction of Dependency Inversion on Swift talk. Also, in our Swift Dependency Inversion through Protocol talk, we share how to achieve Dependency Inversion in Swift. Finally, in the Dependency Injection in Swift talk, we demonstrated the details of dependency Injection. Feel free to visit those for a crystal idea of dependency inversion and injection.

Persistence store case study

We will use the persistence store storage as our case study for this blog post. Our goal is to store some user preferences in some persistence stores like User Defaults. Now, we will do TDD here. And by doing that, we will clarify the Test Doubles in Swift.

We will have three different user preferences. We will be storing those values in User Defaults.


public enum Preference {
    case notNow
    case never
    case save(Credential)
}

Credential is a simple struct with username and password.


public struct Credential {
    public let username: String
    public let password: String
}

Let us start TDD for this case study and explore the Swift test Doubles like Spy, Stub, Dummy, Fake, and Mock.

Implementation through abstraction

Here is the abstraction, protocol, to separate the high-level business login from the low-level details.


protocol PreferenceStorable{
    func save(_ data: Data, for key: String) -> Error?
}

We will have a controller who will be making the business decisions. We can call it StorageController. StorageController will have one instance of type PreferenceStorable. Through this instance, the controller will save the preference. The User Defaults will confirm PreferenceStorable and implement the low-level details (save requirement).

Dependency Diagram

Swift Test Doubles Code Implementation

On the TestDoubles GitHub repo, we have closed Pull requests demonstrating the usage of each test double. We can go through the pull request and select the next or previous button to see the evaluation of the code in TDD.

Spy

The very first thing we want to test is the initialization of the StorageController does not initiate the saving. Then, we will confirm the save of StorageController actually storing the preference. These two scenarios tell us we need to verify the saved value. That sounds like a Spy.

The concept of Spy is straightforward. Spy will only store values, nothing else. We will use this stored value of the Spy to validate the assumption through assertion. Here is an implementation of Spy. Note that return is a method-only requirement. The nil return does not have any relation with the Spy, SpyPreferenceStorage.


class SpyPreferenceStorage: PreferenceStorable {
    private(set) var preferences = [String: Data]()

    func save(_ data: Data, for key: String) -> Error? {
        preferences[key] = data
        return nil
    }
}

So, the required tests will be as follows.


class TestDoublesTests: XCTestCase {
    func test_init_doesnotStorePreference() {
        let (_, spy) = makeSUT()

        XCTAssertTrue(spy.preferences.isEmpty)
    }

    func test_savePreference_storesPref() {
        let (sut, spy) = makeSUT()
        XCTAssertTrue(spy.preferences.isEmpty)

        sut.save(preference: .never)

        XCTAssertFalse(spy.preferences.isEmpty)
    }

    // MARK: - Helper
    private func makeSUT() -> (sut: StorageController, spy: SpyPreferenceStorage) {
        let spy = SpyPreferenceStorage()
        let sut = StorageController(presistenceStore: spy)
        return (sut, spy)
    }
}

The resulting code for the SpyPreferenceStorage will be the following.



struct StorageController {
    let presistenceStore: PreferenceStorable

    func save(preference: Preference){
        let encoder = JSONEncoder()
        guard let data = try? encoder.encode(preference) else { return }
        _ = presistenceStore.save(data, for: Self.PreferenceStoreKey)
    }
}

extension StorageController{
    static var PreferenceStoreKey: String {"PreferenceStoreKey"}
}

Stub

Now, once can confirm the value is stored now let us turn our attention to error handling. The StorageController should be able to handle errors properly. If there is no error then the StorageController should not propagate any error. And if an error occurs the StorageController should propagate that exact error to the caller. So it seems like we need to mimic the erroneous state of the system to validate the error-handling behavior of the StorageController. This is the job for Stub.

Stub returns a predefined response. When we need one component to return some specific value we use a stub. Here is an example of Stub.


struct StubPreferenceStorage: PreferenceStorable {
    let error: Error?

    func save(_ data: Data, for key: String) -> Error? { error }
}

On our StubPreferenceStorage stub we will set the error on init time. If we want some specific error then we will set that error on the init time. Otherwise, we can set the error to nil on init time to reflect a no error state of the system. Let us have a look at the test_save_throwsError_onErroneousStoring test which simulates an erroneous state. On the other hand, the test_save_throwsNoError_onSuccessfulStoring simulates no error state.


final class ResponseAfterStoringTests: XCTestCase {
    func test_save_throwsError_onErroneousStoring() {
        let expectedError = NSError(domain: "any error", code: -100)
        let (sut, _) = makeSUT(expectedError)

        do {
            try sut.save(preference: .never)
            XCTFail("Expected error but got none")
        } catch {
            XCTAssertEqual(error as NSError, expectedError)
        }
    }

    func test_save_throwsNoError_onSuccessfulStoring() {
        let noError: Error? = nil
        let (sut, _) = makeSUT(with: noError)

        do {
            try sut.save(preference: .never)
        } catch {
            XCTFail("Expected no error but got an \(error)")
        }
    }

    // MARK: - Helper
    private func makeSUT(_ error: Error?) -> (sut: StorageController, stub: StubPreferenceStorage){
        let stub = StubPreferenceStorage(error: error)
        let sut = StorageController(presistenceStore: stub)
        return (sut, stub)
    }
}

Here we are setting the StubPreferenceStorage with an NSError. This error will be returned on the save(_ data: Data, for key: String) call. Now let us update our production code StorageController to return any error if occurs.


public struct StorageController {
    private let presistenceStore: PreferenceStorable
    
    public init(presistenceStore: PreferenceStorable) {
        self.presistenceStore = presistenceStore
    }

    public func save(preference: Preference) throws{
        let encoder = JSONEncoder()
        guard let data = try? encoder.encode(preference) else { return }

        if let error = presistenceStore.save(data, for: Self.PreferenceStoreKey) {
            throw error
        }
    }
}

extension StorageController{
    public static var PreferenceStoreKey: String {"PreferenceStoreKey"}
}

As you can see once we have an error in presistenceStore.save we are throwing that error. So using stub we can define the presistenceStore erroneous state which helps us to confirm the production code error handling behavior.

Dummy

Till now we were only concerned with the presistenceStore: PreferenceStorable. But there is still another hidden dependency on the StorageController. The JSONEncoder that we are initiating on save method. If some error occurs during the encoding time we have no way to return that error to the caller. So let us move our focus on that part of the code.

If we want to stub any error during the encoding process we need to inject this dependency, JSONEncoder. So let us take an encoder on the init method. As a result, the StorageController will look like the following.


public struct StorageController {
    private let presistenceStore: PreferenceStorable
    private let encoder: JSONEncoder

    public init(presistenceStore: PreferenceStorable, encoder: JSONEncoder) {
        self.presistenceStore = presistenceStore
        self.encoder = encoder
    }

    public func save(preference: Preference) throws{
        let data: Data
        do{
            data = try encoder.encode(preference)
        }catch{
            throw error
        }

        if let error = presistenceStore.save(data, for: Self.PreferenceStoreKey) {
            throw error
        }
    }
}

Now returning the error state during the encoding process of the JSONEncoder is a stub job. But what role the presistenceStore now will take? The response of save now does not matter. This sounds like a Dummy.

The Dummy is a filler. Dummy always returns a response, that does not have any impact on that specific test scenario. So here is a dummy look at the PreferenceStorable.


class DummyPreferenceStorage: PreferenceStorable {
    func save(_ data: Data, for key: String) -> Error? { nil }
}

On the save method of the DummyPreferenceStorage, we are not concerned about the return param. We just need a PreferenceStorable to initiate the StorageController like following.


let sut = StorageController(presistenceStore: DummyPreferenceStorage(), 
                                    encoder: StubFailableJsonEncoder(error: anyEncodingError))

The implementation of Dummy is visible on the corresponding pull request.

Fake

Among all test doubles, the Fake is different from its kind. Fake has its brain. So, a Fake will return results based on the business logic in it.

One usage can be the dictionary-based implementation of the PreferenceStorable rather than the actual UserDefaults. So UserDefaults has two methods to set and retrieve the data. func set(_ value: Any?, forKey defaultName: String) and func value(forKey key: String) -> Any?. Let us invert the dependency. Rather than depending on UserDefaults let us make our system depend on abstraction, protocol.


protocol UserDefaultsStorage {
    func set(_ value: Any?, forKey defaultName: String)
    func value(forKey key: String) -> Any?
}

The UserDefaultsStorage encapsulate the methods that are required for setting and retrieving the value. So the UserDefaults confirmation will be like the following.


extension UserDefaults: UserDefaultsStorage{}

And now, this should be enough to get going. But if we program against the abstraction, then we will use the UserDefaultsStorage instead of UserDefaults. That way, we can use another test double which can store the value in a dictionary rather than directly saving it to UserDefaults.


class FakeStorage: UserDefaultsStorage{
    var dictionary = [String: Any]()
    
    func set(_ value: Any?, forKey defaultName: String) {
        dictionary[defaultName] = value
    }
    
    func value(forKey key: String) -> Any? {
        dictionary[key]
    }
}

In the above code set and value methods use a dictionary to store and return the value. This is pure business logic to store the value in a non-persistence store rather than a persistence store, ie UserDefaults. So this is a Fake test double as it is using its business logic to store and deliver the stored result from within a Dictionary.

Mock

Mock always looks like Spy. But there is a distinct difference between Spy and Mock. On Spy, we just capture the values. The assertion happens in the corresponding test. On the other hand, in a mock, we do the verification in the mock itself.

In the mock, we expose verifications so that the test can assert the expected behavior. Following is an example of Mock.


class MockPUTAPI: PUTAPI{
    private var isCalled = false
    private var completion: ((Result)-> Void)?
    
    func put(preference: Preference, completion: @escaping (Result)-> Void) {
        isCalled = true
        self.completion = completion
    }
    
    var verifyCall: Bool{
        isCalled == true
    }
    
    var verifyCompletionSetup: Bool{
        completion != nil
    }
}

The verifyCall and verifyCompletionSetup does not explicitly expose the underlying value, but rather just confirms if they are set to an expected value. So now the corresponding tests will just assert these verifications.


func test_init_doesNotInitiateRemoteCall() {
        let (_, mockAPI) = makeSUT()
        
        XCTAssertFalse(mockAPI.verifyCall)
        XCTAssertFalse(mockAPI.verifyCompletionSetup)
    }
    
func test_put_initiateRemoteCall() {
        let (sut, mockAPI) = makeSUT()
        
        sut.executePUTRequest(for: URL(string: "any-url")!, with: .never){_ in}
        
        XCTAssertTrue(mockAPI.verifyCall)
        XCTAssertTrue(mockAPI.verifyCompletionSetup)
    }

All the not-shown entities can be found on GitHub.

Conclusion

Swift Test Doubles such as Spy, Stub, Fake, and Mock gives us the ability to make our test deterministic. But knowing them is not just enough. We also need to know what difference they pose compared to one another. Also when to use which one? All these questions can not be answered in a single blog post. So we will be resuming our test doubles talks in the next talk. Till then take care.

Leave a Reply

Notifications for mobidevtalk! Cool ;) :( not cool