问题
从ObjC / C ++进入Swift世界,编写单元测试时遇到一个问题:缺少用于创建Mock对象的工具。
在编写分解代码时,我们通常将实现细节隐藏在接口(协议)的后面。将对象的组成部分替换为mokami,也非常方便地与其他对象分开检查对象的功能。
谷歌搜索,我在github上找到了几个Swift Mocking框架。但是对我来说,它们在使用中都没有一个清晰明了(出于一个或几个原因):
- 有依赖性云
- 没有直观的语法
- 代码生成/需要大量辅助代码的手动编写
- 未与XCTest集成(允许潮湿,但未经测试)
- 将许多功能转储到一个框架中(混淆了什么和原因)
- 应该与有限数量的预定义/内置类型一起使用
这种情况对我来说是不愉快的,并且大约一年来,我使用了变通办法和自制的Mokas。
自写的模拟对象很简单,但是它们
- 每次都不同(重新发明轮子)
- 或每次相同(复制粘贴)
- 使用不明显
决断
C ++ gTest / gMock ( Google).
Mock- . , «» ( ) .
gMock, sMock (Swift Mock) gMock.
https://github.com/Alkenso/sMock
sMock:
- mock /
- expectations
- - mock-
- XCTest.framework
- zero-dependency
- pure Swift
()
sMock
- Mock , ( )
- Mock methodCall. "" .
Mocking synchronous method
:
import XCTest
import sMock
protocol HTTPClient {
    func sendRequestSync(_ request: String) -> String
}
class MockHTTPClient: HTTPClient {
    
    let sendRequestSyncCall = MockMethod<String, String>()
    func sendRequestSync(_ request: String) -> String {
        
        
        sendRequestSyncCall.call(request) ?? ""
    }
}
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)
        
        
        mock.sendRequestSyncCall
            
            .expect("Request sent.")
            
            .match("{ action: 'retrieve_records' }")
            
            .willOnce(
                
                .return("r1;r2;r3"))
        
        let records = client.retrieveRecordsSync()
        XCTAssertEqual(records, ["r1", "r2", "r3"])
    }
}
Mocking synchronous method + mocking asynchonous callback
Mock callback'a, .
protocol HTTPClient {
    func sendRequestSync(_ request: String) -> String
}
class MockHTTPClient: HTTPClient {
    
    let sendRequestSyncCall = MockMethod<String, String>()
    func sendRequestSync(_ request: String) -> String {
        
        
        sendRequestSyncCall.call(request) ?? ""
    }
}
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)
        
        
        mock.sendRequestSyncCall
            
            .expect("Request sent.")
            
            .match("{ action: 'retrieve_records' }")
            
            .willOnce(
                
                .return("r1;r2;r3"))
        
        
        
        let completionCall = MockClosure<[String], Void>()
        completionCall
            
            .expect("Records retrieved.")
            
            .match(["r1", "r2", "r3"])
            
            
            .willOnce()
        
        client.retrieveRecordsAsync(completion: completionCall.asClosure())
        
        sMock.waitForExpectations()
    }
}
Matchers, Actions, Argument capture
sMock (. ):
- Matchers — , .match: .any ( )
- Actions: expectation match'e .return, .
- : expectation .
- : , unexpectedCall
.
.
: , . .
, Swift .
, sMock . .
"testing with Swift"!
Try sMock on github (with SwiftPackageManager)