Problème
En entrant dans le monde Swift à partir d'ObjC / C ++, j'ai rencontré un problème lors de l'écriture de tests unitaires: le manque d'outils pour créer des objets Mock.
Lors de l'écriture de code décomposé, nous masquons souvent les détails d'implémentation derrière des interfaces (protocoles). Il est également très pratique de vérifier la fonctionnalité d'un objet séparément des autres, en remplaçant ses composants par du mokami.
Sur Google, j'ai trouvé plusieurs frameworks Swift Mocking sur github. Mais aucun d'eux ne m'a paru clair et évident à l'usage (pour une ou plusieurs raisons):
- a un nuage de dépendances
- pas de syntaxe intuitive
- la génération de code / l'écriture manuelle d'une grande quantité de code auxiliaire est requise
- non intégré à XCTest (permet mouillé, mais pas testé)
- d'avoir de nombreuses fonctionnalités dans un tas de framework (confusion de quoi et pourquoi)
- doit être utilisé avec un nombre limité de types prédéfinis / intégrés
Cette situation était désagréable pour moi et pendant environ un an, j'ai utilisé des solutions de contournement et des mokas faits maison.
Les faux objets auto-écrits sont simples, mais ils
- différent à chaque fois (réinventer la roue)
- ou le même à chaque fois (copier-coller)
- peu évident à utiliser
Décision
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)