简化 Combine Publisher 测试并帮助提高单元测试可读性的 XCTestExpectation 子类。
使用 XCTestExpectation
为 Combine Publisher (或 @Published 属性) 编写测试通常涉及一些样板代码,例如
let expectation = XCTestExpectation(description: "Wait for the publisher to emit the expected value")
viewModel.$output.sink { _ in
} receiveValue: { value in
if value == expectedValue {
expectation.fulfill()
}
}
.store(in: &cancellables)
wait(for: [expectation], timeout: 1)
我们可以尝试使用 XCTKeyPathExpectation
,但这要求被观察的对象继承自 NSObject
,并且还需要使用 @objc
属性和 dynamic
修饰符标记我们要观察的属性,以使其符合 KVO 规范
class ViewModel: NSObject {
@objc dynamic var isLoading = false
}
let expectation = XCTKeyPathExpectation(keyPath: \ViewModel.isLoading, observedObject: viewModel, expectedValue: true)
另一种诱人的方法是使用 XCTNSPredicateExpectation
,例如
let expectation = XCTNSPredicateExpectation(predicate: NSPredicate { _,_ in
viewModel.output == expectedValue
}, object: viewModel)
虽然这看起来不错且简洁,但 XCTNSPredicateExpectation
的问题在于它非常慢,最适合用于 UI 测试。这是因为它使用某种轮询机制,该机制会在期望被满足之前至少增加 1 秒的显著延迟。因此,最好不要在单元测试中采用这种方法。
PublisherExpectations 是一组 3 个 XCTestExpectation,允许以清晰简洁的方式声明 publisher 事件的期望。它们继承自 XCTestExpectation,因此可以像任何其他 expectation 一样在 wait(for: [expectations])
调用中使用。
PublisherValueExpectation
:当 publisher 发出一个符合特定条件的值时,该 expectation 会被满足。PublisherFinishedExpectation
:当 publisher 成功完成时,该 expectation 会被满足。PublisherFailureExpectation
:当 publisher 以失败完成时,该 expectation 会被满足。let publisherExpectation = PublisherValueExpectation(stringPublisher, expectedValue: "Got it")
let publisherExpectation = PublisherValueExpectation(arrayPublisher) { $0.contains(value) }
@Published
属性包装器let publisherExpectation = PublisherValueExpectation(viewModel.$isLoaded, expectedValue: true)
let publisherExpectation = PublisherValueExpectation(viewModel.$keywords) { $0.contains("Cool") }
let publisherExpectation = PublisherFinishedExpectation(publisher)
let publisherExpectation = PublisherFinishedExpectation(publisher, expectedValue: 2)
let publisherExpectation = PublisherFinishedExpectation(arrayPublisher) { array in
array.allSatisfy { $0 > 5 }
}
let publisherExpectation = PublisherFailureExpectation(publisher)
let publisherExpectation = PublisherFailureExpectation(publisher) { error in
guard case .apiError(let code) = error, code = 500 else { return false }
return true
}
let publisherExpectation = PublisherFailureExpectation(publisher, expectedError: ApiError(code: 100))
感谢 Combine,我们可以调整 publisher 以检查许多事情,同时保持测试的可读性
let publisherExpectation = PublisherValueExpectation(publisher.collect(3), expectedValue: [1,2,3])
let publisherExpectation = PublisherValueExpectation(publisher.first(), expectedValue: 1)
let publisherExpectation = PublisherValueExpectation(publisher.last(), expectedValue: 5)
let publisherExpectation = PublisherValueExpectation(publisher.dropFirst().first(), expectedValue: 2)