Mock test double: asserting collaboration

When someone is talking about Mock, they probably mean test doubleā€”as you have seen, the differences between the types of test double are really subtle. However, test mock is a well-defined test double. The goal here is to verify the indirect input and generate the indirect output in a precise way.

In the test, we set the expectations, which are the function invocations with the exact parameters, and at the end of the test, we verify the actual versus the expected invocation in the mock. For this example, we still use TaskManager with a store, modifying it to returning the number of tasks after adding one.

Since we need to check the exact parameter passed in the invocation, we add an id property to the task, and we make it Equatable:

struct Task: Equatable {
let id = UUID().uuidString
}

class TaskManager {
private let store: TaskStore

init(store: TaskStore) {
self.store = store
}
func add(task: Task) -> Int {
store.add(task: task)
return store.count
}
var count: Int {
return store.count
}
}

protocol TaskStore {
func add(task: Task)
var count: Int { get }
}

The test will become slightly more verbose because we are setting the expectations at the beginning, and, depending on the number of the interactions with the mock, it could be quite a long list:

func testAddAnCountTasks() {
let firstTaskToBeAdded = Task()
let secondTaskToBeAdded = Task()
let mockTaskStore = MockTaskStore()
mockTaskStore.callAdd(with: firstTaskToBeAdded)
mockTaskStore.callCount(returning: 1)
mockTaskStore.callAdd(with: secondTaskToBeAdded)
mockTaskStore.callCount(returning: 2)

let taskManager = TaskManager(store: mockTaskStore)
_ = taskManager.add(task: firstTaskToBeAdded)
_ = taskManager.add(task: secondTaskToBeAdded)

mockTaskStore.verify()
}

In other languages, such as Java or C#, that have more dynamic characteristics compared to Swift, mocks are usually implemented by libraries that use introspection to generate from a protocol, the functions to implement the verification and the expectations. Due to the static nature of Swift, this isn't possible yet. There have been a few attempts to do it by generating code from protocols, but these are very cumbersome to use, and making manual mocks is still the best option.
Let's see here how to do it for the TaskStore:

class MockTaskStore: TaskStore {
private enum FunctionType {
case add, count
}

private struct FunctionInvocation {
let type: FunctionType
let params: [Any]
}

private var expected = [FunctionInvocation]()
private var actual = [FunctionInvocation]()
private var nextCall: Int = 0

var count: Int {
let currentCall = expected[nextCall]
let returningValue = (currentCall.params[0] as? Int) ?? 0
actual.append(FunctionInvocation(type: .count, params: [returningValue]))

nextCall = expected.index(after: nextCall)
return returningValue
}

func add(task: Task) {
actual.append(FunctionInvocation(type: .add, params: [task]))
nextCall = expected.index(after: nextCall)
}

func callAdd(with task: Task) {
expected.append(FunctionInvocation(type: .add, params: [task]))
nextCall = expected.startIndex
}

func callCount(returning value: Int) {
expected.append(FunctionInvocation(type: .count, params: [value]))
nextCall = expected.startIndex
}

func verify() {
XCTAssertEqual(expected.count, actual.count)
zip(expected, actual).forEach { (expected, actual) in
XCTAssertEqual(expected.type, actual.type)
switch (expected.type, actual.type) {
case (.add, .add):
if let expectedParam = expected.params.first as? Task,
let actualParam = actual.params.first as? Task {
XCTAssertEqual(expectedParam, actualParam)
} else {
XCTFail("Wrong parameter for call of type \(expected.type)")
}
default:
break
}
}
}
}

As you can see, the code for the Mock is very long, and it would be even longer if we'd added more functions to the store. Also, it is a kind of test that rigidly defines the contract between the SUT and the collaborators, not only in the functions that can be called but also on the sequence of the calls; in this way, we are in a certain sense blocking the implementation of the SUT, and we can easily break the encapsulation of the SUT, making it more difficult to refactor and extend the software. For this reason, test mocks must be used sparingly, and you should use other kinds of less invasive test doubles.