sMock

什么是 sMock?

使用 sMock 进行测试非常简单!

  1. 创建实现协议/继承子类/作为回调闭包的 Mock 类;
  2. 设置期望;
  3. 执行测试代码;
  4. 使用 sMock.waitForExpectations() 等待期望

库家族

您还可以找到用于 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()
    }
}

模拟属性 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.
    }
}

匹配器

使用匹配器与模拟实体一起创建精确的期望。
所有匹配器对于 MockMethod、MockClosure 和 MockSetter 都是相同的。

基础

匹配器 描述 示例
.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))
Args: Equatable
匹配器 描述 示例
.equal 实际值 == value .equal(10)
.notEqual 实际值 != value .notEqual(20)
.inCollection<C: Collection> where Args == C.Element 检查项是否在集合中 .inCollection( [10, 20] )
Args: Comparable
匹配器 描述 示例
.greaterEqual 实际值 >= value .greaterEqual(10)
.greater 实际值 > value .greaterEqual(10)
.lessEqual 实际值 <= value .greaterEqual(10)
.less 实际值 < value .greaterEqual(10)
Args == Bool
匹配器 描述 示例
.isTrue 实际值 == true .isTrue()
.isFalse 实际值 == false .isFalse()
Args == String
匹配器 描述 示例
.strCaseEqual 实际值 == value (不区分大小写) .strCaseEqual("sTrInG")
.strCaseNotEqual 实际值 != value (不区分大小写) .strCaseNotEqual("sTrInG")
Args == Result
匹配器 描述 示例
.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)
Args: Collection
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))
Args: Collection, Args.Element: Equatable
匹配器 描述 示例
.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] )

动作

动作用于确定在调用匹配时模拟实体的行为。
所有动作对于 MockMethod、MockClosure 和 MockSetter 都是相同的。

类型

动作 描述 示例
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 配置

sMock 支持自定义配置

属性 描述
unexpectedCallBehavior 确定当发生意外调用时 sMock 将执行什么操作 .warning:仅打印消息到控制台
.failTest:触发 XCTFail
.custom((_ mockedEntity: String) -> Void):调用自定义函数

默认值:.failTest
waitTimeout: TimeInterval 在 'waitForExpectations' 函数中使用的默认超时时间 超时时间,以秒为单位

默认值:0.5

待完成的工作