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: "1.0.0")
]
Import AsyncRecorder in your test files:
import AsyncRecorder
Use 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