使用 sMock 进行测试非常简单!
您还可以找到用于 macOS / *OS 开发的 Swift 库
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()
}
}
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.
}
}
匹配器 | 描述 | 示例 |
---|---|---|
.any | 匹配任何参数 | .any |
.custom( (Args) -> Bool ) | 使用自定义闭包进行匹配 | .custom( { (args) in return true/false } ) |
let mock = MockMethod<Int, Void>()
mock.expect("Method called.").match(<matchers go here>)...
匹配器 | 描述 | 示例 |
---|---|---|
.keyPath<Root, Value> .keyPath<Root, Value: Equatable> |
通过 keyPath 将匹配器应用于实际值 | // Int 中的位数 .keyPath(\.bitWidth, .equal(64)) .keyPath(\.bitWidth, 64) |
.optional | 允许传递 Optional 类型的匹配器。 nil 匹配为 false |
let value: Int? = ... .optional(.equal(value)) |
.splitArgs<T...> | 允许将匹配器分别应用于每个实际值 | // let mock = MockMethod<(Int, String), Void>() .splitArgs(.any, .equal("str")) |
.isNil where Args == Optional .notNil where Args == Optional |
检查实际值是否为 nil/not nil | // let mock = MockMethod<String?, Void>() .isNil() / .isNotNil() |
.cast | 将实际值转换为 T 并对 T 使用匹配器 | // let mock = MockMethod<URLResponse, Void() .cast(.keyPath(\HTTPURLResponse.statusCode, 200)) .cast(to: HTTPURLResponse.self, .keyPath(\.statusCode, 200)) |
匹配器 | 描述 | 示例 |
---|---|---|
.equal | 实际值 == value | .equal(10) |
.notEqual | 实际值 != value | .notEqual(20) |
.inCollection<C: Collection> where Args == C.Element | 检查项是否在集合中 | .inCollection( [10, 20] ) |
匹配器 | 描述 | 示例 |
---|---|---|
.greaterEqual | 实际值 >= value | .greaterEqual(10) |
.greater | 实际值 > value | .greaterEqual(10) |
.lessEqual | 实际值 <= value | .greaterEqual(10) |
.less | 实际值 < value | .greaterEqual(10) |
匹配器 | 描述 | 示例 |
---|---|---|
.isTrue | 实际值 == true | .isTrue() |
.isFalse | 实际值 == false | .isFalse() |
匹配器 | 描述 | 示例 |
---|---|---|
.strCaseEqual | 实际值 == value (不区分大小写) | .strCaseEqual("sTrInG") |
.strCaseNotEqual | 实际值 != value (不区分大小写) | .strCaseNotEqual("sTrInG") |
匹配器 | 描述 | 示例 |
---|---|---|
.success<Success, Failure> | 如果结果是 success,则使用 MatcherType 匹配 success 值。如果是 Result.failure 则为 False | // let mock = MockMethod<Result<Int, Error>, Void>() .success(.equal(10)) |
.failure<Success, Failure> | 如果结果是 failure,则使用 MatcherType 匹配错误。如果是 Result.success 则为 False | .failure(.any) |
let mock = MockMethod<[Int], Void>()
mock.expect("Method called.").match(<matchers go here>)...
匹配器 | 描述 | 示例 |
---|---|---|
.isEmpty | 检查集合是否为空 | .isEmpty() |
.sizeIs | 检查集合的大小是否等于 value | .sizeIs(10) |
.each | 要求集合中的每个元素都匹配 | .each(.greater(10)) |
.atLeastOne | 要求集合中至少有一个元素匹配 | .atLeastOne(.equal(10)) |
匹配器 | 描述 | 示例 |
---|---|---|
.contains | 检查实际集合是否包含元素 | .contains(10) |
.containsAllOf<C: Collection> | 检查实际集合是否包含子集中的所有项 | .containsAllOf( [10, 20] ) |
.containsAnyOf<C: Collection> | 检查实际集合是否包含子集中的任何项 | .containsAnyOf( [10, 20] ) |
.startsWith<C: Collection> | 检查实际集合是否以子集为前缀 | .startsWith( [10, 20] ) |
.endsWith<C: Collection> | 检查实际集合是否以子集为后缀 | .endsWith( [10, 20] ) |
动作 | 描述 | 示例 |
---|---|---|
WillOnce | 期望调用应该发生一次且仅发生一次 | .WillOnce() |
WillRepeatedly | 期望调用应该发生若干次(或无限次) | .WillRepeatedly(.count(10)) |
WillNever | 期望调用永远不应该发生 | .WillNever() |
动作 | 描述 | 示例 |
---|---|---|
.return(R) | 调用模拟实体将返回确切的值 | .return(10) |
.throw(Error) | 调用模拟实体将抛出错误 | .throw(RuntimeError("Something happened.)) |
.perform( (Args) throws -> R ) | 调用模拟实体将执行自定义动作 | .perform( { (args) in return ... } ) |
期望使用 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 支持自定义配置
属性 | 描述 | 值 |
---|---|---|
unexpectedCallBehavior | 确定当发生意外调用时 sMock 将执行什么操作 | .warning:仅打印消息到控制台 .failTest:触发 XCTFail .custom((_ mockedEntity: String) -> Void):调用自定义函数 默认值:.failTest |
waitTimeout: TimeInterval | 在 'waitForExpectations' 函数中使用的默认超时时间 | 超时时间,以秒为单位 默认值:0.5 |