SUT
1 | extension URLRequest { |
前几篇单元测试文章我们有讲到依赖注入,我们继续将MusicService通过依赖项:我们继续改进:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29protocol HTTPClient {
func execute(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void)
}
struct MusicService {
let httpClient: HTTPClient
func search(_ term: String, completion: @escaping (Result<[Track], Error>) -> Void) {
httpClient.execute(request: .search(term: term)) { result in
completion(self.parse(result))
}
}
private func parse(_ result: Result<Data, Error>) -> Result<[Track], Error> { ... }
}
class RealHTTPClient: HTTPClient {
func execute(request: URLRequest, completion: @escaping (Result<Data, Error>) -> Void) {
URLSession.shared.dataTask(with: request) { data, response, error in
DispatchQueue.main.async {
if let data = data {
completion(.success(data))
} else {
completion(.failure(error!))
}
}
}.resume()
}
}
Mocking Data
1 | class MockHTTPClient: HTTPClient { |
接下来,我们校验结果集是否一致1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19func testSearchWithSuccessResponse() throws {
// 1.准备测试数据
let expectedTracks = [Track(trackName: "A", artistName: "B")]
let response = try JSONEncoder().encode(SearchMediaResponse(results: expectedTracks))
// 2.构建mock对象
let httpClient = MockHTTPClient()
httpClient.result = .success(response)
let sut = MusicService(httpClient: httpClient)
var result: Result<[Track], Error>?
// 3.调用search方法,将结果映射给result
sut.search("A") { result = $0 }
// 4.断言比较预期值和结果值
XCTAssertEqual(result?.value, expectedTracks)
}
我们也可验证请求失败的情况:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17func testSearchWithFailureResponse() throws {
// 1.
let httpClient = MockHTTPClient()
httpClient.result = .failure(DummyError())
let sut = MusicService(httpClient: httpClient)
var result: Result<[Track], Error>?
// 2.
sut.search("A") { result = $0 }
// 4.
XCTAssertTrue(result?.error is DummyError)
}
struct DummyError: Error {}
Testing Before & After
我们可以将异步业务分为异步处理之前,异步处理之后
例如刚刚的SUI,我们分为test before1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24func testSearchBefore() {
let sut = MusicService()
// 1.
sut.search("A") { _ in }
// 2.
let lastRequest = URLSession.shared.tasks.last?.currentRequest
XCTAssertEqual(lastRequest?.url, URLRequest.search(term: "A").url)
}
extension URLSession {
var tasks: [URLSessionTask] {
var tasks: [URLSessionTask] = []
let group = DispatchGroup()
group.enter()
getAllTasks {
tasks = $0
group.leave()
}
group.wait()
return tasks
}
}
在处理异步业务之后,MusicService需要处理数据解析。那么可以把这一块作为一个小的UST来进行测试。1
2
3
4
5
6
7
8
9
10func testSearchAfterWithSuccess() throws {
let expectedTracks = [Track(trackName: "A", artistName: "B")]
let response = try JSONEncoder().encode(SearchMediaResponse(results: expectedTracks))
let sut = MusicService()
let result = sut.parse(data: response, error: nil)
XCTAssertEqual(result.value, expectedTracks)
}1
2
3
4
5
6
7
8
9func testSearchAfterWithFailure() {
let sut = MusicServiceWithoutDependency()
let result = sut.parse(data: nil, error: DummyError())
XCTAssertTrue(result.error is DummyError)
}
struct DummyError: Error {}
这么一看,好像写了一堆废代码。咋看都错误匹配。但就是校验错误异常是否为DummyError。看起来有点傻。
Expectations
以上Mock测试方法和Test Before & After 虽然也管用,但始终没有测试到异步代码。XCTest框架提供XCTestExpectation。 这个模式方法总结出以下几个步骤。
- 创建XCTestExpectation实例。
- 当异步任务结束时候完成此期望。
- 等待期望被兑现。
- 断言期望的结果
ok,回到HttpClient的版本,看我们如何编写这个单元测试。1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19func testSearch() {
// 1.构建期望
let didReceiveResponse = expectation(description: #function)
// 与mock不同的是,这里不做mock,直接给真实数据对象
let sut = MusicService(httpClient: RealHTTPClient())
var result: Result<[Track], Error>?
sut.search("ANYTHING") {
result = $0
didReceiveResponse.fulfill() //2. 填充期望
}
// 3.等待期望值....,这一步非常重要。在5秒等待响应结果,内部应该有个loop检查结果是否返回
wait(for: [didReceiveResponse], timeout: 5)
// 4.输出期望结果
XCTAssertNotNil(result?.value)
}
XCTestExpectation 是一个多用途的工具,可以应用于许多场景。1
2
3
4
5
6
7
8
9
10
11
12
13func testInvertedExpectation() {
// 1.
let exp = expectation(description: #function)
exp.isInverted = true
// 2.
sut.maybeComplete {
exp.fulfill()
}
// 3.
wait(for: [exp], timeout: 0.1)
}
Notification Expectation 可用作当通知被接收的校验1
2
3
4
5
6
7
8func testExpectationForNotification() {
let exp = XCTNSNotificationExpectation(name: .init("MyNotification"), object: nil)
...
sut.postNotification()
wait(for: [exp], timeout: 1)
}
以及通过expectedFulfillmentCount属性,可验证fulfill()被调用的次数。1
2
3
4
5
6
7
8
9
10func testExpectationFulfillmentCount() {
let exp = expectation(description: #function)
exp.expectedFulfillmentCount = 3
exp.assertForOverFulfill = true
...
sut.doSomethingThreeTimes()
wait(for: [exp], timeout: 1)
}
Busy Assertion
1 | 通过开启一个runloop,在一定时间范围内循环判断isFulfilled的状态,如果是则退出循环。 |
用法:1
2
3
4
5
6
7
8
9func testSearchBusyAssert() {
let sut = MusicService(httpClient: RealHTTPClient())
var result: Result<[Track], Error>?
sut.search("ANYTHING") { result = $0 }
expectToEventually(result?.value != nil, timeout: 5)
}