When writing unit tests we want to check the interactions between the subject under test or sut
and its dependencies. In other programming languages like Objective-C we can do this with one of the multiple mocking frameworks available such as OCMock. Unfortunately in Swift we don’t have any framework like these available and the reason for this is that Swift’s reflection capabilities are limited, they provide read-only access to a subset of type metadata. In other words, we can’t modify a class definition in runtime. This leaves us with two options: Write mocks manually or generate them with some short of code generator. Today we’ll talk about the first option.
Writing mocks manually
Protocols are very useful when writing mocks. A protocol defines a blueprint of methods, properties and other requirements that adopting classes, structs or enumerations must implement. A protocol can be adopted by one or several classes.
Let’s say we have the following scenario: A presenter can fetch a user through a repository and update a view when the user is retrieved. We can define the user, the view and the repository as follows:
struct User: Equatable {
let name: String
let lastname: String
}
protocol UserView {
func update(user: User)
}
protocol UserRepository {
typealias UserCallback = (User) -> Void
func fetchUser(result: @escaping UserCallback)
}
The actual implementations for the repository and the view are irrelevant for this demonstration. Let’s now create our presenter which will fetch the user’s data and update the view.
class UserPresenter {
private let view: UserView
private let repository: UserRepository
init(view: UserView, repository: UserRepository) {
self.view = view
self.repository = repository
}
func loadUser() {
repository.fetchUser { user in
self.view.update(user: user)
}
}
}
So far so good. We can see now how UserPresenter
depends and interacts with both the repository and the view. The next step will be testing those interactions. To do this, we’ll go to our test target and create two new classes: UserViewMock
and UserRepositoryMock
. They will implement the protocols that we defined earlier which will allow us to inject them into UserPresenter
.
class UserViewMock: UserView {
var updateParam: User?
func update(user: User) {
updateParam = user
}
}
class UserRepositoryMock: UserRepository {
var fetchUserParam: UserCallback?
var fetchUserWasCalled = false
func fetchUser(result: @escaping UserCallback) {
fetchUserParam = result
fetchUserWasCalled = true
}
}
We added control variables to the mocks so we can later check from the unit tests that certain functions were called and what kind of parameters were passed to them.
We can now create instances of UserViewMock
and UserRepositoryMock
, inject them into UserPresenter
and validate that the interactions work as expected. We can even fake a response from the fetching function and make it return any data we want.
class UserPresenterTests: XCTestCase {
var view: UserViewMock!
var repository: UserRepositoryMock!
var sut: UserPresenter!
override func setUp() {
super.setUp()
view = UserViewMock()
repository = UserRepositoryMock()
sut = UserPresenter(view: view, repository: repository)
}
func testLoadUser() {
sut.loadUser()
XCTAssertTrue(repository.fetchUserWasCalled)
let expecedUser = User(name: "Alex", lastname: "Salom")
repository.fetchUserParam?(expecedUser)
XCTAssertEqual(view.updateParam, expecedUser)
}
}
Pretty nice, don’t you think?
But… what about unowned code?
We saw that writing mocks for code that we own is very easy, we only need to make our production and mock classes adopt the same protocol. But what happens with code which we do not own and we can’t modify?
Once again, we need to use protocols. We need to extend the code that we do not own with our own protocols. Let’s see this in action with a simple Counter
that relies on UserDefaults
.
protocol Defaults {
func integer(forKey defaultName: String) -> Int
func set(_ value: Int, forKey defaultName: String)
}
extension UserDefaults: Defaults { }
class Counter {
private let defaults: Defaults
init(defaults: Defaults = UserDefaults.standard) {
self.defaults = defaults
}
var count: Int {
return defaults.integer(forKey: "count")
}
func increment() {
defaults.set(count + 1, forKey: "count")
}
}
See? We decorated Apple’s UserDefaults
with our own protocol named Defaults
where we defined functions with the same signature that UserDefaults
has. Counter
then declares a Defaults
instance variable that gets injected through the initializer using a default value of UserDefaults.standard
but with type Defaults
; which is our own protocol. We can now use the exact same technique as before to implement our own DefaultsMock
in the test target.
class DefaultsMock: Defaults {
var integerParam: String?
var integerToReturn = 0
var integerWasCalled = false
func integer(forKey defaultName: String) -> Int {
integerWasCalled = true
integerParam = defaultName
return integerToReturn
}
var setParams: (value: Int, defaultName: String)?
func set(_ value: Int, forKey defaultName: String) {
setParams = (value, defaultName)
}
}
Notice how we used an optional touple in the set
function as a control variable where we can group all parameters that are passed into the function. This will reduce the amount of variables we need to define. Instead of defining one control variable per parameter in the funciton’s signature, we define a touple that groups them all.
With all these in place we can write our tests that verify how our Counter
implementation interacts with UserDefaults
. We’ll write two tests, one will verify how the count is obtained from the UserDefaults
and another one that will verify how increasing the counter will first obtain the value from the UserDefaults
, increase it by one and then save it again in the UserDefaults
.
class CounterTests: XCTestCase {
var defaults: DefaultsMock!
var sut: Counter!
override func setUp() {
super.setUp()
defaults = DefaultsMock()
sut = Counter(defaults: defaults)
}
func testRetrieveCount() {
let retrunedCount = sut.count
XCTAssertEqual(retrunedCount, defaults.integerToReturn)
XCTAssertEqual(defaults.integerParam, "count")
}
func testIncrementCount() {
let beforeIncrement = defaults.integerToReturn
sut.increment()
XCTAssertTrue(defaults.integerWasCalled)
XCTAssertEqual(defaults.setParams?.value, beforeIncrement + 1)
XCTAssertEqual(defaults.setParams?.defaultName, "count")
}
}
This is how you can mock code which you don’t have access and can’t modify.