sMock - Marco de burla rápida para pruebas unitarias (gracias gMock por ideas)

Problema


Al pasar al mundo Swift desde ObjC / C ++, me encontré con un problema al escribir pruebas unitarias: la falta de herramientas para crear objetos Mock.


Al escribir código descompuesto, a menudo ocultamos detalles de implementación detrás de las interfaces (protocolos). También es muy conveniente verificar la funcionalidad de un objeto por separado de otros, reemplazando sus partes componentes con mokami.


Buscando en Google, encontré varios frameworks Swift Mocking en github. Pero ninguno de ellos me pareció claro y obvio en uso (por una o varias razones):


  • tiene una nube de dependencias
  • sin sintaxis intuitiva
  • Se requiere la generación de código / escritura manual de una gran cantidad de código auxiliar
  • no integrado con XCTest (permite mojado, pero no probado)
  • tener muchas características volcadas en un solo marco (confusión de qué y por qué)
  • debe usarse con un número limitado de tipos predefinidos / incorporados

Esta situación fue desagradable para mí, y durante aproximadamente un año utilicé soluciones y mokas hechos a mí mismo.
Los objetos simulados escritos por ellos mismos son simples, pero


  • diferente cada vez (reinventar la rueda)
  • o lo mismo cada vez (copiar y pegar)
  • no obvio para usar

Decisión


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