Swift mock-helping library written with gMock (C++) library approach in mind;
uses XCTestExpectations inside, that makes sMock not only mocking library, but also library that allows easy unit-test coverage of mocked objects expected behavior;
lightweight and zero-dependecy;
works out-of-the-box without need of generators, tools, etc;
required minimum of additional code to prepare mocks.
Testing with sMock is simple!
Create Mock class implementing protocol / subclassing / as callback closure;
Make expectations;
Execute test code;
Wait for expectations using sMock.waitForExpectations()
Library family
You can also find Swift libraries for macOS / *OS development
SwiftConvenience: Swift common extensions and utilities used in everyday development
sXPC: Swift type-safe wrapper around NSXPCConnection and proxy object
sLaunchctl: Swift API to register and manage daemons and user-agents
Examples
Mocking synchronous method
import XCTest
import sMock
// Protocol to be mocked.
protocol HTTPClient {
func sendRequestSync(_ request: String) -> String
}
// Mock implementation.
class MockHTTPClient: HTTPClient {
// Define call's mock entity.
let sendRequestSyncCall = MockMethod<String, String>()
func sendRequestSync(_ request: String) -> String {
// 1. Call mock entity with passed arguments.
// 2. If method returns non-Void type, provide default value for 'Unexpected call' case.
sendRequestSyncCall.call(request) ?? ""
}
}
// Some entity to be tested.
struct Client {
let httpClient: HTTPClient
func retrieveRecordsSync() -> [String] {
let response = httpClient.sendRequestSync("{ action: 'retrieve_records' }")
return response.split(separator: ";").map(String.init)
}
}
class ExampleTests: XCTestCase {
func test_Example() {
let mock = MockHTTPClient()
let client = Client(httpClient: mock)
// Here we expect that method 'sendRequestSync' will be called with 'request' argument equals to "{ action: 'retrieve_records' }".
// We expect that it will be called only once and return "r1;r2;r3" as 'response'.
mock.sendRequestSyncCall
// Assign name for exact expectation (useful if expectation fails);
.expect("Request sent.")
// This expectation will only be trigerred if argument passed as parameter equals to passed Matcher;
.match("{ action: 'retrieve_records' }")
// Assume how many times this method with this arguments (defined in 'match') should be called.
.willOnce(
// If method for this expectation called, it will return value we pass in .return(...) statement.
.return("r1;r2;r3"))
// Client internally requests records using HTTPClient and then parse response.
let records = client.retrieveRecordsSync()
XCTAssertEqual(records, ["r1", "r2", "r3"])
}
}
// Protocol to be mocked.
protocol HTTPClient {
func sendRequestSync(_ request: String) -> String
}
// Mock implementation.
class MockHTTPClient: HTTPClient {
// Define call's mock entity.
let sendRequestSyncCall = MockMethod<String, String>()
func sendRequestSync(_ request: String) -> String {
// 1. Call mock entity with passed arguments.
// 2. If method returns non-Void type, provide default value for 'Unexpected call' case.
sendRequestSyncCall.call(request) ?? ""
}
}
// Some entity to be tested.
struct Client {
let httpClient: HTTPClient
func retrieveRecordsAsync(completion: @escaping ([String]) -> Void) {
let response = httpClient.sendRequestSync("{ action: 'retrieve_records' }")
completion(response.split(separator: ";").map(String.init))
}
}
class ExampleTests: XCTestCase {
func test_Example() {
let mock = MockHTTPClient()
let client = Client(httpClient: mock)
// Here we expect that method 'sendRequestSync' will be called with 'request' argument equals to "{ action: 'retrieve_records' }".
// We expect that it will be called only once and return "r1;r2;r3" as 'response'.
mock.sendRequestSyncCall
// Assign name for exact expectation (useful if expectation fails);
.expect("Request sent.")
// This expectation will only be trigerred if argument passed as parameter equals to passed Matcher;
.match("{ action: 'retrieve_records' }")
// Assume how many times this method with this arguments (defined in 'match') should be called.
.willOnce(
// If method for this expectation called, it will return value we pass in .return(...) statement.
.return("r1;r2;r3"))
// Here we use 'MockClosure' mock entity to ensure that 'completion' handler is called.
// We expect it will be called only once and it's argument is ["r1", "r2", "r3"].
let completionCall = MockClosure<[String], Void>()
completionCall
// Assign name for exact expectation (useful if expectation fails);
.expect("Records retrieved.")
// This expectation will only be trigerred if argument passed as parameter equals to passed Matcher;
.match(["r1", "r2", "r3"])
// Assume how many times this method with this arguments (defined in 'match') should be called.
.willOnce()
// Client internally requests records using HTTPClient and then parse response.
// Returns response in completion handler.
client.retrieveRecordsAsync(completion: completionCall.asClosure())
// Don't forget to wait for potentially async operations.
sMock.waitForExpectations()
}
}
// Protocol to be mocked.
protocol HTTPClient {
func sendRequestAsync(_ request: String, reply: @escaping (String) -> Void)
}
// Mock implementation.
class MockHTTPClient: HTTPClient {
// Define call's mock entity.
let sendRequestAsyncCall = MockMethod<(String, (String) -> Void), Void>()
func sendRequestAsync(_ request: String, reply: @escaping (String) -> Void) {
// Call mock entity with passed arguments.
sendRequestAsyncCall.call(request, reply)
}
}
// Some entity to be tested.
struct Client {
let httpClient: HTTPClient
func retrieveRecordsAsync(completion: @escaping ([String]) -> Void) {
httpClient.sendRequestAsync("{ action: 'retrieve_records' }") { (response) in
completion(response.split(separator: ";").map(String.init))
}
}
}
class ExampleTests: XCTestCase {
func test_Example() {
let mock = MockHTTPClient()
let client = Client(httpClient: mock)
// Here we expect that method 'sendRequestAsync' will be called with 'request' argument equals to "{ action: 'retrieve_records' }".
// We expect that it will be called only once and return "r1;r2;r3" as 'response'.
mock.sendRequestAsyncCall
// Assign name for exact expectation (useful if expectation fails);
.expect("Request sent.")
// This expectation will only be trigerred if argument passed as parameter equals to passed Matcher;
.match(
// SplitArgs allows to apply different Matcher to each argument (splitting tuple);
.splitArgs(
// Matcher for first argument (here: request);
.equal("{ action: 'retrieve_records' }"),
// Matcher for second argument (here: reply block);
.any))
// Assume how many times this method with this arguments (defined in 'match') should be called.
.willOnce(
// If method for this expectation called, it will perform specific handler with all arguments of the call.
// (Here: when mached, it will call 'reply' closure with argument "r1;r2;r3").
.perform({ (_, reply) in
reply("r1;r2;r3")
}))
// Here we use 'MockClosure' mock entity to ensure that 'completion' handler is called.
// We expect it will be called only once and it's argument is ["r1", "r2", "r3"].
let completionCall = MockClosure<[String], Void>()
completionCall
// Assign name for exact expectation (useful if expectation fails);
.expect("Records retrieved.")
// This expectation will only be trigerred if argument passed as parameter equals to passed Matcher;
.match(["r1", "r2", "r3"])
// Assume how many times this method with this arguments (defined in 'match') should be called.
.willOnce()
// Client internally requests records using HTTPClient and then parse response.
// Returns response in completion handler.
client.retrieveRecordsAsync(completion: completionCall.asClosure())
// Don't forget to wait for potentially async operations.
sMock.waitForExpectations()
}
}
Mocking property setter
protocol SomeProtocol {
var value: Int { get set }
}
class Mock: SomeProtocol {
let valueCall = MockSetter<Int>("value", -1)
var value: Int {
get { valueCall.callGet() }
set { valueCall.callSet(newValue) }
}
}
class ExampleTests: XCTestCase {
func test_Example() {
let mock = Mock()
XCTAssertEqual(mock.value, -1)
// Set expectations for setter calls. We expect only value 'set'.
// Get may be called any number of times.
mock.valueCall.expect("First value did set.").match(.less(10)).willOnce()
mock.valueCall.expect("Second value did set.").match(.any).willOnce()
mock.valueCall.expect("Third value did set.").match(.equal(1)).willOnce()
mock.value = 4
XCTAssertEqual(mock.value, 4)
mock.value = 100500
XCTAssertEqual(mock.value, 100500)
mock.value = 1
XCTAssertEqual(mock.value, 1)
// At the end of the test, if any expectation has not been trigerred, it will fail the test.
}
}
Matchers
Use matchers with mock entity to make exact expectations. All matchers are the same for MockMethod, MockClosure and MockSetter.
Basic
Matcher
Description
Example
.any
Matches any argument
.any
.custom( (Args) -> Bool )
Use custom closure to match
.custom( { (args) in return true/false } )
Predifined
let mock = MockMethod<Int, Void>()
mock.expect("Method called.").match(<matchers go here>)...
.inCollection<C: Collection> where Args == C.Element
Checks if item is in collection
.inCollection( [10, 20] )
Args: Comparable
Matcher
Description
Example
.greaterEqual
actual >= value
.greaterEqual(10)
.greater
actual > value
.greaterEqual(10)
.lessEqual
actual <= value
.greaterEqual(10)
.less
actual < value
.greaterEqual(10)
Args == Bool
Matcher
Description
Example
.isTrue
actual == true
.isTrue()
.isFalse
actual == false
.isFalse()
Args == String
Matcher
Description
Example
.strCaseEqual
actual == value (case insensitive)
.strCaseEqual(“sTrInG”)
.strCaseNotEqual
actual != value (case insensitive)
.strCaseNotEqual(“sTrInG”)
Args == Result
Matcher
Description
Example
.success<Success, Failure>
If result is success, matches success value using MatcherType. False if Result.failure
// let mock = MockMethod<Result<Int, Error>, Void>() .success(.equal(10))
.failure<Success, Failure>
If result is failure, matches error using MatcherType. False if Result.success
.failure(.any)
Args: Collection
let mock = MockMethod<[Int], Void>()
mock.expect("Method called.").match(<matchers go here>)...
Matcher
Description
Example
.isEmpty
Checks if collection is empty
.isEmpty()
.sizeIs
Checks that size of collection equals value
.sizeIs(10)
.each
Requires each element of collection to be matched
.each(.greater(10))
.atLeastOne
Required at least on element of collection to be matched
.atLeastOne(.equal(10))
Args: Collection, Args.Element: Equatable
Matcher
Description
Example
.contains
Checks if actual collection contains element
.contains(10)
.containsAllOf<C: Collection>
Checks if actual collection contains all items from subset
.containsAllOf( [10, 20] )
.containsAnyOf<C: Collection>
Checks if actual collection contains any item from subset
.containsAnyOf( [10, 20] )
.startsWith<C: Collection>
Checks if actual collection is prefixed by subset
.startsWith( [10, 20] )
.endsWith<C: Collection>
Checks if actual collection is suffixed by subset
.endsWith( [10, 20] )
Actions
Actions are used to determine mocked entity behavior if call was matched. All actions are the same for MockMethod, MockClosure and MockSetter.
Types
Action
Description
Example
WillOnce
Expect call should be made once and only once
.WillOnce()
WillRepeatedly
Expect call should be made some number of times (or unlimited)
.WillRepeatedly(.count(10))
WillNever
Expect call should be never made
.WillNever()
Actions
Action
Description
Example
.return(R)
Call to mocked entity will return exact value
.return(10)
.throw(Error)
Call to mocked entity will throw error
.throw(RuntimeError(“Something happened.))
.perform( (Args) throws -> R )
Call to mocked entity will perform custom action
.perform( { (args) in return … } )
Capturing arguments
Proper argument capturing expected to be made using ArgumentCaptor.
let mock = MockMethod<Int, Void>()
let captor = ArgumentCaptor<Int>()
mock.expect("Method called.").match(.any).capture(captor).willOnce()
print(captor.captured) // All captured values or empty is nothing captured.
let initedCaptor = InitedArgumentCaptor<Int>(-1)
mock.expect("Method called.").match(.any).capture(initedCaptor).willOnce()
print(initedCaptor.lastCaptured) // Last captured value or default value if nothing captured.
sMock configuration
sMock support custom configuration
Property
Description
Values
unexpectedCallBehavior
Determines what will sMock do when unexpected call is made
.warning: just print message to console .failTest: XCTFail is triggerred .custom((_ mockedEntity: String) -> Void): custom function is called
Default value: .failTest
waitTimeout: TimeInterval
Default timeout used in ‘waitForExpectations’ function
sMock
What is sMock?
Testing with sMock is simple!
Library family
You can also find Swift libraries for macOS / *OS development
Examples
Mocking synchronous method
Mocking synchronous method + mocking asynchonous callback
Mocking asynchronous method + mocking asynchonous callback
Mocking property setter
Matchers
Use matchers with mock entity to make exact expectations.
All matchers are the same for MockMethod, MockClosure and MockSetter.
Basic
Predifined
Miscellaneous
.keyPath<Root, Value: Equatable>
.keyPath(\.bitWidth, .equal(64))
.keyPath(\.bitWidth, 64)
nil matches as false
.optional(.equal(value))
.splitArgs(.any, .equal(“str”))
.notNil
.isNil() / .isNotNil()
.cast(.keyPath(\HTTPURLResponse.statusCode, 200))
.cast(to: HTTPURLResponse.self, .keyPath(\.statusCode, 200))
Args: Equatable
Args: Comparable
Args == Bool
Args == String
Args == Result
.success(.equal(10))
Args: Collection
Args: Collection, Args.Element: Equatable
Actions
Actions are used to determine mocked entity behavior if call was matched.
All actions are the same for MockMethod, MockClosure and MockSetter.
Types
Actions
Capturing arguments
Proper argument capturing expected to be made using ArgumentCaptor.
sMock configuration
sMock support custom configuration
.failTest: XCTFail is triggerred
.custom((_ mockedEntity: String) -> Void): custom function is called
Default value: .failTest
Default value: 0.5
Work to be done