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.