SwiftMock
SwiftMock is a mocking framework for Swift 5.2.
Notes on the history of this repo
- September 2015: first version of this framework
- November 2016: Marked the project as "unmaintained", with a comment "just write fakes instead"
- November 2018: Rewrote this for Swift 4.2, with much simpler code
- May 2020: Minor changes
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!
Versioning
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.
Limitations
- Developers need to be aware of the difference between calls which should be mocked, and those which shouldn't (ie, simple stubs)
- if the function in the collabotor class performs an operation (starting a network request, logging-out a user, starting a timer), then it's good for mocking
- if the function returns a value which your system-under-test uses to make a decision, and that decision is asserted elsewhere in your test code, and the function doesn't have any side-effects, then that function is not a suitable candidate for mocking
- No built-in support for stubbing calls. (This is calls which return a given value, but their use isn't asserted by the mock object)
- You may sometimes need to customise the arguments which are passed into the
accept
call - Not all test-failure scenarios can report exactly where the failure occurred
- It's possible for calls to get confused if a mock has two functions with the same name and similar arguments - but that seems unlikely to me. (Example:
object.function(42)
andobject.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 callingself.button.setEnabled(enabled)
- so your test would probably assert the state of the button is correct, and there's no need to check thatisButtonEnabled()
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.
Usage
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)
}
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])
}
}
Then create the mock in your test class, using MockFrood.create()
. 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 ifvoidFunction()
wasn't called exactly once with the value42
- the mock will fast-fail the test if any other (unexpected) function is called on the mock
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.
Currently-supported syntax
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
mockObject.expect { o in o.functionReturningString() }.returning("dent")
...
mockObject.verify()
// expect a call on a function which returns a String value, and also call a block
mockObject.expect { o in
o.functionReturningString()
}.doing { actionArgs in
print("frood")
}.returning("dent")
...
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?
Various ways to call the "accept" 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(arg) here might not work well - so you can
// select the important (identifying) parts of the struct
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])
}
}
// 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)
Installation
The code is all in one file - so the easiest way to use SwiftMock is to simply copy Mock.swift
into your project.
Feedback
Issues and pull-requests most welcome.
Author
Matthew Flint, [email protected]
License
SwiftMock is available under the MIT license. See the LICENSE file for more info.