Simple helpers for unit testing in Swift
Just add both FakeEquatable.swift and HasInvocations.swift to your project's test target
- Mock and verify
- Assert equal for non equatable value types (CAUTION: not for use with reference types (classes))
Let's say one needs to mock and verify something similar to
struct Thing {
let value: Int
let message: String
}
protocol SomeOutput {
func didSomething()
func didReceive(_ thing: Thing)
func didFail(_ error: Error)
}
with a following usage:
final class Service {
enum FetchError: Error {
case unknown
}
let output: SomeOutput
init(_ output: SomeOutput) {
self.output = output
}
func doSomething() {
output.didSomething()
}
func fetchFromDB() {
output.didFail(FetchError.unknown)
}
func fetchFromNetwork() {
let result = Thing(value: 42, message: "Answer")
output.didReceive(result)
}
func fetchAndDoSomething() {
let result = Thing(value: 42, message: "Answer")
output.didReceive(result)
output.didSomething()
}
}
In order to do so
- Create a mock class that corresponds to
SomeOutput
andHasInvocations
:
final class MockSomeOutput: SomeOutput, HasInvocations {
enum Invocation: FakeEquatable {
case didSomething
case didReceive(Thing)
case didFail(Error)
}
var invocations: [Invocation] = []
func didSomething() {
invocations.append(.didSomething)
}
func didReceive(_ thing: Thing) {
invocations.append(.didReceive(thing))
}
func didFail(_ error: Error) {
invocations.append(.didFail(error))
}
}
- Use previously created mock in unit tests:
final class ServiceTests: XCTestCase {
var output: MockSomeOutput!
var service: Service!
override func setUp() {
super.setUp()
output = MockSomeOutput()
service = Service(output)
}
override func tearDown() {
service = nil
output = nil
super.tearDown()
}
func testDoSomething() {
// when
service.doSomething()
// then
output.checkInvocations([.didSomething])
}
func testFetchFromDB() {
// given
let expectedError = Service.FetchError.unknown
// when
service.fetchFromDB()
// then
output.checkInvocations([.didFail(expectedError)])
}
func testFetchFromNetwork() {
// given
let expectedThing = Thing(value: 42, message: "Answer")
// when
service.fetchFromNetwork()
// then
output.checkInvocations([.didReceive(expectedThing)])
}
func testFetchAndDoSomething() {
// given
let expectedThing = Thing(value: 42, message: "Answer")
// when
service.fetchAndDoSomething()
// then
output.checkInvocations([.didReceive(expectedThing),
.didSomething])
}
}
FakeEquatableWrapper
can be used in case of data structure is not Equatable
but needs to be tested with XCTAssertEqual
inside unit test:
func testIsEqual() {
let firstThing = Thing(value: 42, message: "Answer")
let secondThing = Thing(value: 42, message: "Question")
let wrappedFirstThing = FakeEquatableWrapper(firstThing)
let wrappedSecondThing = FakeEquatableWrapper(secondThing)
XCTAssertEqual(wrappedFirstThing, wrappedSecondThing)
}
To react on invocation changes one can do something like this inside a mock:
var invocations: [Invocation] = [] {
didSet {
invocationsDidChange?(self)
}
}
var invocationsDidChange: ((MockClass) -> Void)?
and use a callback in unit tests:
output.invocationsDidChange = {
//
}