Swift, XCTest

Mock Local Variables for Unit Testing

In this article, we take a look at how to mock local variables for unit testing. Consider the following scenario,

class User {
    func getLanguagePreference() -> String? {
        return UserDefaults.standard.string(forKey: "preferredLanguage")
    }
}

Here we have a User class that has a function named getLanguagePreference which returns the language preference set by the user. In these types of scenarios, one would generally create a class property for UserDefaults and set them with a mock object while unit testing.

In the case of scenarios where an object is used inside only one function, it is not necessary to expose that object as a class property. Instead, we could use Swift Generics to create local variables and mock them. This would be very handy in cases where an object is used only in one function.

For this, first, we create another class named InstanceProvider and use that class to return the created object

class InstanceProvider{
    func getLocalInstance<T>;(instance:T) -> T {
        return instance
    }
}

We receive an object as an argument in getLocalInstance function and then return that as it is to the calling place. Now we modify the getLanguagePreference as follows

class User {
    var instanceProvider = InstanceProvider()
    
    func getLanguagePreference() -> String? {
        let defaults = instanceProvider.getLocalInstance(instance: UserDefaults.standard)
        return defaults.string(forKey: "preferredLanguage")
    }
}

Here we create a local variable defaults using the getLocalInstance by passing the UserDefaults object as its argument. It would return the same object as it is, and we assign it to the local variable.

This would provide an additional layer that we can use to override and return a mock instance in our test method.

Inside Test Target

Inside the test target, we create two files MockUserDefaults.swift and MockInstanceProvider.swift where we mock the two classes individually.

class MockUserDefaults: UserDefaults {
    var mockStringForKey:String?
    
    override func string(forKey defaultName: String) -> String? {
        return mockStringForKey
    }
}
protocol InstanceReturnable: AnyObject {
    func get<T>(instance:T) -> T
}

class MockInstanceProvider: InstanceProvider {
    weak var instanceProviderDelegate:InstanceReturnable!
    
    override func getLocalInstance<T>(instance: T) -> T {
        instanceProviderDelegate.get(instance: instance)
    }
}

We create a instanceProviderDelegate so that our Unit test class can return the mockUserDefaults instance, which we shall see below.

class UserTests: XCTestCase {
    var sut:User!
    var mockDefaults: MockUserDefaults!
    var mockInstanceProvider: MockInstanceProvider!

    override func setUpWithError() throws {
        mockDefaults = MockUserDefaults()
		    mockInstanceProvider = MockInstanceProvider()
				mockInstanceProvider.instanceProviderDelegate = self
        sut = User()
        sut.instanceProvider = mockInstanceProvider
    }

    override func tearDownWithError() throws {
        sut = nil
        mockDefaults = nil
				mockInstanceProvider = nil
    }
}

We create mock objects in setUpWithError() and set self as instance, provider delegate. This will enable us to return our mockDefaults as the mock object for the local variable. Now we shall extend and define the get<T>() method.

extension UserTests: InstanceReturnable {
    func get<T>(instance: T) -> T {
        switch instance {
        case is UserDefaults:
	          return mockDefaults as! T
        default:
            return instance
        }
    }
}

Here we take the instance and check its type. Based on the type, we return the appropriate mock object. This would enable us to handle multiple mock objects by adding cases for each type.

Now that we have all the dependencies set up, we shall write the test method.

func testGetLanguagePreference() {
		//given
		mockDefaults.mockStringForKey = "Some String"

		//when
		let returnValue = sut.getLanguagePreference()

		//then
		XCTAssertEqual(returnValue, "Some String")
}

As you can see, we can use this method to inject mock objects for local variables and use that for testing. Right now, since there is only one local variable, this might not look ideal; however, this will be very handy when there are a lot of local variables created in the project.