sMock-用于单元测试的Swift模拟框架(感谢gMock的想法)

问题


从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
}

//  Mock 
class MockHTTPClient: HTTPClient {
    //   call's mock entity.
    let sendRequestSyncCall = MockMethod<String, String>()

    func sendRequestSync(_ request: String) -> String {
        //  1.    Mock-
        //  2.  non-void   "default"    'Unexpected call' (..     ,   ).
        sendRequestSyncCall.call(request) ?? ""
    }
}

//  ,    
struct Client {
    let httpClient: HTTPClient

    //  ,  HTTP ,    .
    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)

        //     expectation,    'sendRequestSync'  HTTPClient     'request'  "{ action: 'retrieve_records' }".
        //   ,      1     "r1;r2;r3".
        mock.sendRequestSyncCall
            //    expectation (      );
            .expect("Request sent.")
            //  ,   expectation    ,      match
            .match("{ action: 'retrieve_records' }")
            //    ,        (   match)
            .willOnce(
                //  ,    .
                .return("r1;r2;r3"))

        //   expectations,   Client-    .
        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
}

//  Mock 
class MockHTTPClient: HTTPClient {
    //   call's mock entity.
    let sendRequestSyncCall = MockMethod<String, String>()

    func sendRequestSync(_ request: String) -> String {
        //  1.    Mock-
        //  2.  non-void   "default"    'Unexpected call' (..     ,   ).
        sendRequestSyncCall.call(request) ?? ""
    }
}

//  ,    
struct Client {
    let httpClient: HTTPClient

    //  ,  HTTP ,    .   
    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)

        //     expectation,    'sendRequestSync'  HTTPClient     'request'  "{ action: 'retrieve_records' }".
        //   ,      1     "r1;r2;r3".
        mock.sendRequestSyncCall
            //    expectation (      );
            .expect("Request sent.")
            //  ,   expectation    ,      match
            .match("{ action: 'retrieve_records' }")
            //    ,        (   match)
            .willOnce(
                //  ,    .
                .return("r1;r2;r3"))

        //    expectation    ( ). 
        //     ,     -  :  'MockClosure' mock entity.
        //     expectation,      1    ( ) ["r1", "r2", "r3"].
        let completionCall = MockClosure<[String], Void>()
        completionCall
            //    expectation (      );
            .expect("Records retrieved.")
            //  ,   expectation    ,      match
            .match(["r1", "r2", "r3"])
            //    ,        (   match).
            //   return value   Void, .return   .
            .willOnce()

        //   expectations,   Client-.
        client.retrieveRecordsAsync(completion: completionCall.asClosure())

        //      expectations (  " "    ).
        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)


All Articles