おそらく現在はユニットテストでCombineのPublisherの値を検査するためにはXCTestExpectationを駆使したりしないといけません(簡便な方法があったら教えて欲しいです。)。さらに私はSwiftCheckを利用しているので検査結果をBool値で取得したいという要求があります。
ですのでテストターゲットでexpectとexpectErrorという検査メソッドをPublisherに追加しました。
import XCTest
import Combine
public extension Publisher where Output: Equatable {
@discardableResult
func expect(_ expectedValue: Output,
takesNewest: Bool = false,
timeout: TimeInterval = 2.0,
fulfillmentCount: Int = 1,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) -> Bool {
var result = false
var actualValues = [Output]()
var actualFulfillmentCount = 0
var cancellables = Set<AnyCancellable>()
let exp = XCTestExpectation()
exp.expectedFulfillmentCount = fulfillmentCount
exp.assertForOverFulfill = true
let waiter = XCTWaiter()
self.receive(on: RunLoop.main).sink(receiveCompletion: { _ in }, receiveValue: {
if takesNewest || !result {
result = $0 == expectedValue
}
actualValues.append($0)
actualFulfillmentCount += 1
exp.fulfill()
}).store(in: &cancellables)
_ = waiter.wait(for: [exp], timeout: timeout)
XCTAssertLessThanOrEqual(fulfillmentCount,
actualFulfillmentCount,
"\(file) - \(function):\(line): Expectation is short of fulfillment. '\(fulfillmentCount)' expected, but '\(actualFulfillmentCount)'.")
XCTAssertTrue(result,
"\(file) - \(function):\(line): Expected output is '\(String(describing: expectedValue))', but actual stream is '\(String(describing: actualValues))'")
return result && fulfillmentCount <= actualFulfillmentCount
}
}
public extension Publisher where Failure: Equatable {
@discardableResult
func expectError(_ expectedError: Failure,
timeout: TimeInterval = 2.0,
file: StaticString = #file,
function: StaticString = #function,
line: UInt = #line) -> Bool {
var actualError: Failure?
var cancellables = Set<AnyCancellable>()
let exp = XCTestExpectation()
let waiter = XCTWaiter()
self.receive(on: RunLoop.main).sink(receiveCompletion: { completion in
switch completion {
case .failure(let receivedError):
actualError = receivedError
default: ()
}
exp.fulfill()
}, receiveValue: { _ in }).store(in: &cancellables)
_ = waiter.wait(for: [exp], timeout: timeout)
XCTAssertEqual(expectedError, actualError,
"\(file) - \(function):\(line): Expected error is '\(String(describing: expectedError))', but actual error is '\(String(describing: actualError))'")
return expectedError == actualError
}
}
expectする値が取得できれば成功、できなければ失敗、そして結果のBool値を返すようになっています。
timeoutは値を取得する制限時間です。
expectのtakesNewestは、trueであればストリームの最新の値まで全て検査し、過去に該当する値があっても結果を上書きします。fulfillmentCountは何回値を取得するかを設定します。
テストで利用するには以下のように記述します。
class MyTests: XCTestCase {
class P: Publisher {
typealias Output = Int
typealias Failure = Never
func receive<S>(subscriber: S) where S : Subscriber, P.Failure == S.Failure, P.Output == S.Input {
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
subscriber.receive(1)
subscriber.receive(2)
subscriber.receive(completion: .finished)
}
let subscription = Subscriptions.empty
subscriber.receive(subscription: subscription)
}
}
class Q: Publisher {
typealias Output = Int
typealias Failure = MyError
func receive<S>(subscriber: S) where S : Subscriber, Q.Failure == S.Failure, Q.Output == S.Input {
DispatchQueue.global().asyncAfter(deadline: .now() + 1.0) {
subscriber.receive(completion: .failure(.error1))
}
let subscription = Subscriptions.empty
subscriber.receive(subscription: subscription)
}
}
enum MyError: Error, Equatable {
case error1
case error2
}
func testExpect() {
P().expect(1)
P().buffer(size: 2, prefetch: .keepFull, whenFull: .dropOldest).collect().expect([1, 2])
P().expect(1, takesNewest: true, fulfillmentCount: 2) // failed
P().expect(3) // failed
}
func testExpectError() {
Q().expectError(.error1)
}
}
私のプロジェクトでは一応動作しているようです。
バグやより良い実装があれば教えてください。