AsyncRecorder enables testing of Combine publishers using await in SwiftTesting setups. The publisher will be collected into a async stream of data. Each element can be fetched one after another using next. Methods like expect, expectFinished and expectFailure helps to parse and tests the results easily.
Add AsyncRecorder as a dependency in your Package.swift file:
dependencies: [
.package(url: "https://github.com/StatusQuo/AsyncRecorder", from: "2.0.0")
]Import AsyncRecorder in your test files:
import AsyncRecorderUse record() on a Combine publisher to create an AsyncRecorder, and use await for asynchronous testing:
import Testing
import Combine
import AsyncRecorder
struct PublisherTests {
@Test
func test() async throws {
let subject = CurrentValueSubject<Int, Never>(0)
let recorder = subject.record() //Create new AsyncRecorder
subject.send(1)
await recorder.expect(0, 1) //Expect a sequence of values
}
}To wait for a Publisher to "finish" use expectCompletion.
@Test func testExpectCompletion() async throws {
let subject = PassthroughSubject<Int, Never>()
let recorder = subject.record()
subject.send(0)
subject.send(1)
subject.send(completion: .finished)
await recorder
.expect(0, 1)
.expectFinished()
}Errors can be expected with expectError.
@Test func testExpectError() async throws {
let subject = PassthroughSubject<Int, Never>()
let recorder = subject.record()
subject.send(0)
subject.send(1)
subject.send(completion: .failure(.random))
await #expect(throws: TestError.random) {
try await recorder
.expect(0, 1)
.expectFailure()
}
}A Void Publisher can be expected by expectInvocation.
@Test
func testExpectInvocation() async throws {
let subject = PassthroughSubject<Void, Never>()
let recorder = subject.record()
subject.send(())
subject.send(())
subject.send(())
await recorder.expectInvocation(3)
}Skip values you do not want to test
@Test
func testExpectInvocation() async throws {
let progress = PassthroughSubject<Int, Never>()
let recorder = subject.record()
subject.send(0)
...
subject.send(100)
await recorder.skipping().expect(100)
}skipping() will reset after each expect is successfully reached. It also works with expectCompletion() and expectFailure()
For all other cases and more control you can manually pull one element form the stack with next(). This method will fail with an expectation if the next element is not a value.
@Test
func testExpectInvocation() async throws {
let subject = PassthroughSubject<ViewUpdate, Never>()
let recorder = subject.record()
subject.send(.init(progress: 40, callback: nil))
await #expect(recorder.next()?.progress == 40)
}This library is based on "ST K"s stackoverflow answer: https://stackoverflow.com/a/78506360