Problem
Moving into the Swift world from ObjC / C ++, I ran into a problem when writing unit tests: the lack of tools for creating Mock objects.
When writing decomposed code, we often hide implementation details behind interfaces (protocols). It is also very convenient to check the functionality of an object separately from others, replacing its component parts with mokami.
Googling, I found several Swift Mocking frameworks on github. But none of them appeared to me clear and obvious in use (for one or several reasons):
- has a cloud of dependencies
- no intuitive syntax
- code generation / manual writing of a large amount of auxiliary code is required
- not integrated with XCTest (allows wet, but not tested)
- to have many features dumped into a bunch of one framework (confusion of what and why)
- should be used with a limited number of predefined / built-in types
This situation was unpleasant for me, and for about a year I used workarounds and self-made mokas.
Self-written mock objects are simple, but they
- different every time (reinvent the wheel)
- or the same each time (copy-paste)
- unobvious to use
Decision
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)