sMock - إطار المحاكاة السريع لاختبارات الوحدة (شكرًا gMock للأفكار)

مشكلة


عند الانتقال إلى عالم Swift من ObjC / C ++ ، واجهت مشكلة عند كتابة اختبارات الوحدة: نقص الأدوات لإنشاء كائنات وهمية.


عند كتابة شفرة متحللة ، غالبًا ما نخفي تفاصيل التنفيذ خلف الواجهات (البروتوكولات). كما أنه من الملائم جدًا التحقق من وظيفة كائن بشكل منفصل عن الآخرين ، واستبدال الأجزاء المكونة به بموكامي.


Googling ، وجدت العديد من إطارات عمل Swift Mocking على github. ولكن لم يظهر لي أي منها واضحًا وواضحًا في الاستخدام (لسبب واحد أو عدة أسباب):


  • سحابة من التبعيات
  • لا بناء جملة بديهي
  • مطلوب إنشاء رمز / الكتابة اليدوية لكمية كبيرة من التعليمات البرمجية المساعدة
  • غير متكامل مع XCTest (يسمح بالبلل ، ولكن لم يتم اختباره)
  • لإلقاء العديد من الميزات في مجموعة من إطار واحد (الخلط بين ماذا ولماذا)
  • يجب استخدامها مع عدد محدود من الأنواع المحددة مسبقًا / المدمجة

كان هذا الوضع مزعجًا بالنسبة لي ، ولمدة عام تقريبًا ، استخدمت الحلول البديلة والموك ذاتي الصنع.
كائنات وهمية مكتوبة ذاتيا بسيطة ، لكنها


  • مختلفة في كل مرة (إعادة اختراع العجلة)
  • أو نفس كل مرة (نسخ ولصق)
  • غير واضح للاستخدام

القرار


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