sMock - Kerangka kerja mengejek Swift untuk Tes unit (terima kasih gMock untuk ide-ide)

Masalah


Pindah ke dunia Swift dari ObjC / C ++, saya mengalami masalah ketika menulis unit test: kurangnya alat untuk membuat objek Mock.


Saat menulis kode terurai, kami sering menyembunyikan detail implementasi di belakang antarmuka (protokol). Juga sangat mudah untuk memeriksa fungsionalitas suatu objek secara terpisah dari yang lain, mengganti bagian-bagian komponennya dengan mokami.


Googling, saya menemukan beberapa kerangka kerja Swift Mocking di github. Tetapi tidak ada satu pun dari mereka yang tampak jelas dan jelas digunakan oleh saya (karena satu atau beberapa alasan):


  • memiliki awan dependensi
  • tidak ada sintaks yang intuitif
  • pembuatan kode / penulisan manual dari sejumlah besar kode tambahan diperlukan
  • tidak terintegrasi dengan XCTest (memungkinkan basah, tetapi tidak diuji)
  • untuk memiliki banyak fitur dibuang ke sekelompok kerangka kerja (kebingungan tentang apa dan mengapa)
  • harus digunakan dengan jumlah terbatas dari tipe yang telah ditentukan / bawaan

Situasi ini tidak menyenangkan bagi saya, dan selama sekitar satu tahun saya menggunakan solusi dan moka buatan sendiri.
Objek tiruan yang ditulis sendiri itu sederhana, tetapi itu


  • berbeda setiap waktu (reinvent the wheel)
  • atau sama setiap kali (copy-paste)
  • tidak terlihat digunakan

Keputusan


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