Mocks + Fixtures for Swift. Testing is now even easier. Powered by generics.
Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.
A test fixture is a fixed state of a set of objects used as a baseline for running tests. The purpose of a test fixture is to ensure that there is a well known and fixed environment in which tests are run so that results are repeatable.
Add the following lines to your Podfile:
target "Tests" do
use_frameworks!
pod 'Moxture'
end
Run pod install
to integrate the library to your tests target. For details of the installation and usage of
CocoaPods, please visit the official website.
For the protocol that is defined like this:
protocol Example {
func update()
func currentCount(name: String) -> Int
func updateIfNeeded(name: String, count: Int) -> Bool
}
For each function there should be a separate lazy property that defines FuncMock
for given function. The type of
input and output arguments for FuncMock
will be deducted from mocked function type.
The mock should be implemented like this:
class ExampleMock: Example {
lazy var updateFunc = FuncMock(self.update)
func update() {
updateFunc.call()
}
lazy var currentCountFunc = FuncMock(self.currentCount)
func currentCount(name: String) -> Int {
currentCountFunc.call(name)
}
lazy var updateIfNeededFunc = FuncMock(self.updateIfNeeded)
func updateIfNeeded(name: String, count: Int) -> Bool {
updateIfNeededFunc.call((name, count))
}
}
If protocol defines properties:
protocol Example {
// ...
var count: Int { get set }
}
There's a special PropMock
that used specifically for properties:
class ExampleMock: Example {
// ...
lazy var countProp = PropMock(\ExampleMock.count)
var count: Int {
get { countProp.get.call() }
set { countProp.set.call(newValue) }
}
}
Note that due to the bug in current Swift version \Self.count
notation can't be used.
This is the example of the FuncMock
usage in the real test case:
import Moxture
import XCTest
final class ExampleTests: XCTestCase {
func testExample() {
// given
let mock = ExampleMock()
// return value must be set for function which returns any value
// for `Fixturable` return type it will be set as `.fixture` automatically
mock.updateIfNeededFunc.returns = true
let sut = SystemUnderTest(example: mock)
// when
sut.updateExample() // this function calls example mock `updateIfNeeded(...)`
sut.updateExample() // let's call two times
// then
XCTAssertTrue(mock.updateIfNeededFunc.called)
// now `mock.updateIfNeededFunc.calls` contains two tuples of call args [("name", 0), ("name", 0)]
XCTAssertEqual(mock.updateIfNeededFunc.calls.count, 2)
// Assert last call using `args`
XCTAssertEqual(mock.updateIfNeededFunc.args.0, "name")
XCTAssertEqual(mock.updateIfNeededFunc.args.1, 0)
// `reset()` clears calls in the function mock
mock.updateIfNeededFunc.reset()
XCTAssertTrue(mock.updateIfNeededFunc.calls.isEmpty)
}
}
When we need to execute closure in a mock as a part of test we can use onCall
:
protocol Example {
func update(completion: (Bool) -> Void)
}
class ExampleMock: Example {
lazy var updateFunc = FuncMock(self.update)
func update(completion: (Bool) -> Void) {
updateFunc.call(completion)
}
}
final class ExampleTests: XCTestCase {
func testExample() {
// given
let mock = ExampleMock()
// intercept the function call, get completion closure argument and call it with `true`
mock.updateFunc.onCall {
$0(true)
}
let sut = SystemUnderTest(example: mock)
// when
sut.update() // this calls mock.update(completion:)
// then
// mock successfully called the completion closure
XCTAssertTrue(sut.updateFinished())
}
}
For throwing functions we should use separate FuncMock
method callThrows(...)
(throwing functions can't be
distinct from regular ones by Swift compiler):
protocol Example {
func update() throws
}
class ExampleMock: Example {
lazy var updateFunc = FuncMock(self.update)
func update() throws {
try updateFunc.callThrows()
}
}
final class ExampleTests: XCTestCase {
func testExampleThrows() {
// given
let mock = ExampleMock()
// set error to make the function throw, it will be thrown as soon as `update()` is called
mock.updateFunc.throws = ExampleError()
let sut = SystemUnderTest(example: mock)
// when / then
// `try sut.update()` calls example's update method
XCTAssertThrowsError(try sut.update())
}
}
If function defines any generic parameters then it can't be inferred by FuncMock
. In this case we should use
AnyFuncMock
:
protocol Example {
func update<Arg, Result>(object: Arg) -> Result
}
class ExampleMock: Example {
lazy var updateFunc = AnyFuncMock() // doesn't take the function as argument
func update<Arg, Result>(object: Arg) -> Result {
updateFunc.call(object) as! Result // the execution will be failed if there's a type mismatch
}
}
final class ExampleTests: XCTestCase {
func testExample() {
// given
let mock = ExampleMock()
// return value must be set
mock.updateFunc.returns = 123
let sut = SystemUnderTest(example: mock)
// when
sut.update() // this calls mock.update(...)
// then
// check if function was called with specific arguments,
// use `args(of:)` to cast last call arguments to specific type
XCTAssertEqual(mock.updateFunc.args(of: Int.self), 123)
}
}
AnyFuncMock
doesn't guarantee the generic type match, it should be controlled by the mock side.
To use fixtures in the test code the data structure or class should extend Fixturable
:
struct Example {
var int: Int
var string: String
var double: Double
}
extension Example: Fixturable {
static func fixture(label: String, configure: (inout Example) -> Void) -> Example {
makeFixture(label, configure, self.init)
}
}
We don't need to re-implement the Fixturable
implementation if we add extra fields. We should specify initializer
if there are multiple initializers - self.init(arg1:arg2:)
.
Function makeFiture
recursively apply fixtures to all fields. All fields in the Fixturable
structure should be also
Fixturable
.
Moxture implements Fixturable
for most of the built in Swift and Foundation types.
Simple usage of the fixture is this:
final class ExampleTests: XCTestCase {
func testExample() {
// given
let data: Example = .fixture
let sut = SystemUnderTest()
// when
sut.update(data: data) // saves the data as `lastData`
// then
// all fixture instances are equal to each other
XCTAssertEqual(sut.lastData, .fixture)
// all fields in the fixturable data replaced by .fixture as well
XCTAssertEqual(sut.lastData.int, .fixture)
XCTAssertEqual(sut.lastData.string, .fixture)
}
}
There's a closure for configuring the fields after instantiation:
final class ExampleTests: XCTestCase {
func testExample() {
// given
let data: Example = .fixture {
// configure a property with custom value
$0.string = "custom string"
}
let sut = SystemUnderTest()
// when
sut.update(data: data) // saves the data as `lastData`
// then
// now the property of the data structure is "custom string"
XCTAssertEqual(sut.lastData.string, .fixture { $0.string = "custom string" })
// ...and not equal to `.fixture` anymore
XCTAssertNotEqual(sut.lastData.string, .fixture)
}
}
We should use .fixture(label: "identifier")
, if we need multiple fixtures which should be distinct from each other:
final class ExampleTests: XCTestCase {
func testExample() {
// given
let newData: Example = .fixture(label: "new")
let oldData: Example = .fixture(label: "old")
let sut = SystemUnderTest()
sut.lastData = oldData
// when
sut.lockData() // should prevent from updating `lastData`
sut.update(data: newData) // try to update data
// then
// properties of two fistures with different labels are not equal to each other
XCTAssertNotEqual(sut.lastData.string, newData.string)
}
}
In this example .fixture(label: "new")
is not equal to .fixture(label: "old")
. There's a limitation for structures
with fewer fields or with no fields. For example, if there's only one Bool
property then two fixtures with different
labels are still equal to each other, because default implementation of Bool: Fixturable
extension always returns
true
to prevent unexpected results in tests.
In general, .fixture
(or fixture with specific label) is guaranteed to be the identical during the program run. But
there's no guarantee that those values will be the same among different program / test runs. It uses hashValue
under
the hood to produce values based on a specific label, it has same guarantees as hashValue
.
Mocks and fixtures can be easily combined with each other:
final class ExampleTests: XCTestCase {
func testExample() {
// given
let mock = ExampleMock()
// assign return value of the mock as fixture
mock.updateFunc.returns = .fixture {
// override id in the generated fixture
$0.id = "custom id"
}
let sut = SystemUnderTest(example: mock)
// when
sut.update(.fixture(label: "result")) // send specific fixture as argument
// then
// assert by comparing with other fixtures
XCTAssertEqual(mock.updateFunc.agrs, .fixture(label: "result"))
}
}
If FuncMock
or PropMock
have Fixturable
return type, then it is not required to explicitly set returns
,
it will be automatically set as .fixture
.
Your PRs are always welcome.
Fixturable
implementations for different Swift frameworks.