A mocking framework for Swift
SwiftMock is a mocking framework for Swift 5.9.
I spent a while using fakes (test-doubles which implement a prototol and simply set various methodWasCalled
flags), but this doesn’t scale well. It’s easy to forget to make assertions, especially if a new function is added to a protocol long after the protocol’s fake was written. I’ve since migrated a lot of code to using this new Mock, and it’s amazing how many defects I’ve found. Mocks FTW!
SwiftMock versions track the major/minor version of Swift itself, so you can easily find a tag for a version of SwiftMock which works with your version of Swift.
accept
callobject.function(42)
and object.function("42")
)The decision whether to “mock or stub” depends on what the function does. As an example, these two functions have similar method signatures:
func isButtonEnabled() -> Bool
func saveValuesToKeychain() -> Bool
Both have no arguments, and both return a Bool - but they are very different:
isButtonEnabled()
returns a boolean based on some internal logic. Your system-under-test will probably take that boolean value and use it to do something else, like calling self.button.setEnabled(enabled)
- so your test would probably assert the state of the button is correct, and there’s no need to check that isButtonEnabled()
is called. We care that the outcome is correct, not how the system-under-test decided to do the correct thing. This doesn’t need to be mocked.saveValuesToKeychain()
is a command; our system-under-test is asking the mocked collaborator to do some useful work. In this case, we really do care that the function is called, so it should be mocked.The examples below assume we’re mocking this protocol:
protocol Frood {
func voidFunction(value: Int)
func functionReturningString() -> String
func performAction(value: String, completion: @escaping () -> Void)
func throwingFunction() throws -> String
}
In your test target you’ll need to create a MockFrood
which extends Mock
with a generic type parameter Frood
. It must also adopt the Frood
protocol.
class MockFrood: Mock<Frood>, Frood {
func voidFunction(value: Int) {
accept(args: [value])
}
func functionReturningString() -> String {
return accept() as! String
}
func performAction(value: String, completion: @escaping () -> Void) {
accept(checkArgs: [value], actionArgs: [completion])
}
func throwingFunction() throws -> String {
try throwingAccept() as! String
}
}
Then create the mock in your test class, using MockFrood.create()
. You may pass in a name for this mock, if you don’t like the default name. A test class would typically look something like this:
class MyTests: XCTestCase {
private let mockFrood = MockFrood.create()
private func verify(file: StaticString = #file,
line: UInt = #line) {
mockFrood.verify(file: file, line: line)
}
func test_something() {
// given
let thing = MyThing(frood: mockFrood)
// expect
mockFrood.expect { f in f.voidFunction(value: 42) }
// when
thing.hoopy()
// then
verify()
}
This gives you the following behaviour:
verify()
will fail the test if voidFunction()
wasn’t called exactly once with the value 42
The original version of SwiftMock had explicit matcher classes for various types; this newer version simply converts each argument to a String
, and matches on those String
arguments. You can often simply accept
the arguments themseves, but sometimes you’ll want to be more specific about what you pass into the accept
function.
I’d probably put the mock objects in a separate group in the test part of my project.
SwiftMock syntax requires the expected call in a block; this might look weird at first, but means that we have a readable way of setting expectations, and we know the return value before the expectation is set.
// expect a call on a void function
mockObject.expect { o in o.voidFunction(42) }
...
mockObject.verify()
// expect a call on a function which returns a String value
// this uses the $0 _shorthand argument name_, if you prefer that style
mockObject.expect { $0.functionReturningString() }
.returning("dent")
...
mockObject.verify()
// expect a call on a function which returns a String value, and also call
// a block when the expectation is satisfied
mockObject.expect { o in
o.functionReturningString()
}.doing { actionArgs in
print("frood")
}.returning("dent")
...
mockObject.verify()
// expect a call on a function which should throw instead of returning
// a value
mockObject.expect { try $0.functionReturningString() }
.throwing(Error.vogons)
...
mockObject.verify()
Mocks are strict. This means they will reject any unexpected call. If this annoys you, then perhaps you should be stubbing those calls instead of mocking?
accept
or throwingAccept
function when writing your Mock object// simplest use-case - mocking a function which takes no arguments and
// returns no value
class Mock<Protocol>, Protocol {
func myFunc() {
accept()
}
}
// mocking a function which returns a value
// this uses a force-cast, but in test code I guess we can live with it
class Mock<Protocol>, Protocol {
func myFunc() -> String {
return accept() as! String
}
}
// mocking a function which takes some arguments which must match the
// expected values. SwiftMock will convert each argument to a String
// and match them all
class Mock<Protocol>, Protocol {
func myFunc(personName: String, yearOfBirth: Int) {
accept(args: [personName, yearOfBirth])
}
}
// mocking a function which takes parameter with a custom type
struct Employee {
let personID: UUID
let personName: String
let roles: [Role]
}
class Mock<Protocol>, Protocol {
func myFunc(employee: Employee) {
// calling `accept(args: [employee])` here will not work well - so
// you should pass the important (identifying) parts of the struct
// instead
accept(args: [employee.personID, employee.personName])
}
}
// specifying the function name explicitly
// SwiftMock's accept call has a func parameter with a default value of
// #func. This works most of the time, but you may wish to override it
class Mock<Protocol>, Protocol {
func myFunc(value: Int) {
accept(func: "functionName", args: [value])
}
}
// you can set expectations that a property is set by using `didSet`
// in your Mock object
protocol ProtocolWithProperty: AnyObject {
var value: Int { get set }
}
class MockObject: Mock<ProtocolWithProperty>, ProtocolWithProperty {
var value: Int = 0 {
didSet {
accept(args: [value])
}
}
}
// usage in the test class
class ExampleTests: XCTestCase {
let mock = MockObject.create()
func test_withProperty() {
// expect
mock.expect { $0.value = 42 }
// when
...
// then
mock.verify()
}
}
// arguments which are unknown - example: a completion block which should be captured
// here, the "url" argument is known by the test (and we expect it to be correct),
// but the "completion" block argument must be captured by the test. So "checkArgs"
// are used to check the function was called with the expected parameters, while
// "actionArgs" are ignored by the matching code, and passed into any "doing" block
class Mock<Protocol>, Protocol {
func getWebContent(url: URL, completion: (Data) -> Void) -> RequestState {
return accept(checkArgs: [url], actionArgs:[completion]) as! RequestState
}
}
// usage in the test class
var capturedCompletionBlock: ((Data) -> Void)?
myMock.expect { m in
m.getWebContent(url: expectedURL, completion: { data in })
}.doing { actionArgs in
capturedCompletionBlock = actionArgs[0] as? (Data) -> Void
}.returning(.requesting)
// then later, call that captured block to simulate an incoming response
capturedCompletionBlock?(response)
The code is all in one file - so the easiest way to use SwiftMock is to simply copy Mock.swift
into your project.
Alternatively, use Swift Package Manager.
Issues and pull-requests most welcome.
Matthew Flint
SwiftMock is available under the MIT license. See the LICENSE file for more info.