import XCTest
import CombineExpectations
class PublisherTests: XCTestCase {
func testElements() throws {
// 1. Create a publisher
let publisher = ...
// 2. Start recording the publisher
let recorder = publisher.record()
// 3. Wait for a publisher expectation
let elements = try wait(for: recorder.elements, timeout: ..., description: "Elements")
// 4. Test the result of the expectation
XCTAssertEqual(elements, ["Hello", "World!"])
}
}
When you wait for a publisher expectation:
The test fails if the expectation is not fulfilled within the specified timeout.
An error is thrown if the expected value can not be returned. For example, waiting for recorder.elements throws the publisher error if the publisher completes with a failure.
The wait method returns immediately if the expectation has already reached the waited state.
You can wait multiple times for a publisher:
class PublisherTests: XCTestCase {
func testPublisher() throws {
let publisher = ...
let recorder = publisher.record()
// Wait for first element
_ = try wait(for: recorder.next(), timeout: ...)
// Wait for second element
_ = try wait(for: recorder.next(), timeout: ...)
// Wait for successful completion
try wait(for: recorder.finished, timeout: ...)
}
}
Not all tests have to wait, because some publishers expectations are fulfilled right away. In this case, prefer the synchronous get() method over wait(for:timeout:), as below:
class PublisherTests: XCTestCase {
func testSynchronousPublisher() throws {
// 1. Create a publisher
let publisher = ...
// 2. Start recording the publisher
let recorder = publisher.record()
// 3. Grab the expected result
let elements = try recorder.elements.get()
// 4. Test the result of the expectation
XCTAssertEqual(elements, ["Hello", "World!"])
}
}
Just like wait(for:timeout:), the get() method can be called multiple times:
class PublisherTests: XCTestCase {
// SUCCESS: no error
func testPassthroughSubjectSynchronouslyPublishesElements() throws {
let publisher = PassthroughSubject<String, Never>()
let recorder = publisher.record()
publisher.send("foo")
try XCTAssertEqual(recorder.next().get(), "foo")
publisher.send("bar")
try XCTAssertEqual(recorder.next().get(), "bar")
}
}
Installation
Add a dependency for CombineExpectations to your Swift Package test targets:
// SUCCESS: no timeout, no error
func testArrayPublisherCompletesWithSuccess() throws {
let publisher = ["foo", "bar", "baz"].publisher
let recorder = publisher.record()
let completion = try wait(for: recorder.completion, timeout: ...)
if case let .failure(error) = completion {
XCTFail("Unexpected error \(error)")
}
}
// SUCCESS: no error
func testArrayPublisherSynchronouslyCompletesWithSuccess() throws {
let publisher = ["foo", "bar", "baz"].publisher
let recorder = publisher.record()
let completion = try recorder.completion.get()
if case let .failure(error) = completion {
XCTFail("Unexpected error \(error)")
}
}
recorder.elements waits for the recorded publisher to complete.
When waiting for this expectation, a RecordingError.notCompleted is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails.
✅ Otherwise, an array of published elements is returned.
recorder.last waits for the recorded publisher to complete.
When waiting for this expectation, a RecordingError.notCompleted is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails.
✅ Otherwise, the last published element is returned, or nil if the publisher completes before it publishes any element.
// SUCCESS: no timeout, no error
func testArrayPublisherPublishesLastElementLast() throws {
let publisher = ["foo", "bar", "baz"].publisher
let recorder = publisher.record()
if let element = try wait(for: recorder.last, timeout: ...) {
XCTAssertEqual(element, "baz")
} else {
XCTFail("Expected one element")
}
}
// SUCCESS: no error
func testArrayPublisherSynchronouslyPublishesLastElementLast() throws {
let publisher = ["foo", "bar", "baz"].publisher
let recorder = publisher.record()
if let element = try recorder.last.get() {
XCTAssertEqual(element, "baz")
} else {
XCTFail("Expected one element")
}
}
Examples of failing tests
// FAIL: Asynchronous wait failed
// FAIL: Caught error RecordingError.notCompleted
func testLastTimeout() throws {
let publisher = PassthroughSubject<String, Never>()
let recorder = publisher.record()
let element = try wait(for: recorder.last, timeout: ...)
}
// FAIL: Caught error MyError
func testLastError() throws {
let publisher = PassthroughSubject<String, MyError>()
let recorder = publisher.record()
publisher.send(completion: .failure(MyError()))
let element = try wait(for: recorder.last, timeout: ...)
}
next()
recorder.next() waits for the recorded publisher to emit one element, or to complete.
When waiting for this expectation, a RecordingError.notEnoughElements is thrown if the publisher does not publish one element after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next element.
✅ Otherwise, the next published element is returned.
recorder.next(count) waits for the recorded publisher to emit count elements, or to complete.
When waiting for this expectation, a RecordingError.notEnoughElements is thrown if the publisher does not publish count elements after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next count elements.
✅ Otherwise, an array of exactly count elements is returned.
recorder.single waits for the recorded publisher to complete.
When waiting for this expectation, a RecordingError is thrown if the publisher does not complete on time, or does not publish exactly one element before it completes. The publisher error is thrown if the publisher fails.
✅ Otherwise, the single published element is returned.
// SUCCESS: no timeout, no error
func testJustPublishesExactlyOneElement() throws {
let publisher = Just("foo")
let recorder = publisher.record()
let element = try wait(for: recorder.single, timeout: ...)
XCTAssertEqual(element, "foo")
}
// SUCCESS: no error
func testJustSynchronouslyPublishesExactlyOneElement() throws {
let publisher = Just("foo")
let recorder = publisher.record()
let element = try recorder.single.get()
XCTAssertEqual(element, "foo")
}
Examples of failing tests
// FAIL: Asynchronous wait failed
// FAIL: Caught error RecordingError.notCompleted
func testSingleTimeout() throws {
let publisher = PassthroughSubject<String, Never>()
let recorder = publisher.record()
let element = try wait(for: recorder.single, timeout: ...)
}
// FAIL: Caught error MyError
func testSingleError() throws {
let publisher = PassthroughSubject<String, MyError>()
let recorder = publisher.record()
publisher.send(completion: .failure(MyError()))
let element = try wait(for: recorder.single, timeout: ...)
}
// FAIL: Caught error RecordingError.tooManyElements
func testSingleTooManyElementsError() throws {
let publisher = PassthroughSubject<String, Never>()
let recorder = publisher.record()
publisher.send("foo")
publisher.send("bar")
publisher.send(completion: .finished)
let element = try wait(for: recorder.single, timeout: ...)
}
// FAIL: Caught error RecordingError.notEnoughElements
func testSingleNotEnoughElementsError() throws {
let publisher = PassthroughSubject<String, Never>()
let recorder = publisher.record()
publisher.send(completion: .finished)
let element = try wait(for: recorder.single, timeout: ...)
}
Combine Expectations
Utilities for tests that wait for Combine publishers.
Latest release: version 0.10.0 (August 11, 2021) • Release Notes
Requirements: iOS 13+, macOS 10.15+, and tvOS 13+ require Swift 5.1+ or Xcode 11+. watchOS 7.4+ requires Swift 5.4+ or Xcode 12.5+.
Contact: Report bugs and ask questions in Github issues.
Testing Combine publishers with XCTestExpectation often requires setting up a lot of boilerplate code.
CombineExpectations aims at streamlining those tests. It defines an XCTestCase method which waits for publisher expectations.
Usage
Waiting for Publisher Expectations allows your tests to look like this:
When you wait for a publisher expectation:
recorder.elements
throws the publisher error if the publisher completes with a failure.wait
method returns immediately if the expectation has already reached the waited state.You can wait multiple times for a publisher:
Not all tests have to wait, because some publishers expectations are fulfilled right away. In this case, prefer the synchronous
get()
method overwait(for:timeout:)
, as below:Just like
wait(for:timeout:)
, theget()
method can be called multiple times:Installation
Add a dependency for CombineExpectations to your Swift Package test targets:
Publisher Expectations
There are various publisher expectations. Each one waits for a specific publisher aspect:
availableElements
recorder.availableElements
waits for the expectation to expire, or the recorded publisher to complete.✅ Otherwise, an array of all elements published before the expectation has expired is returned.
Unlike other expectations,
availableElements
does not make a test fail on timeout expiration. It just returns the elements published so far.Example:
completion
recorder.completion
waits for the recorded publisher to complete.RecordingError.notCompleted
is thrown if the publisher does not complete on time.✅ Otherwise, a
Subscribers.Completion
is returned.Example:
Examples of failing tests
elements
recorder.elements
waits for the recorded publisher to complete.RecordingError.notCompleted
is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails.✅ Otherwise, an array of published elements is returned.
Example:
Examples of failing tests
finished
recorder.finished
waits for the recorded publisher to complete.Example:
Examples of failing tests
recorder.finished
can be inverted:Examples of failing tests
last
recorder.last
waits for the recorded publisher to complete.RecordingError.notCompleted
is thrown if the publisher does not complete on time, and the publisher error is thrown if the publisher fails.✅ Otherwise, the last published element is returned, or nil if the publisher completes before it publishes any element.
Example:
Examples of failing tests
next()
recorder.next()
waits for the recorded publisher to emit one element, or to complete.RecordingError.notEnoughElements
is thrown if the publisher does not publish one element after last waited expectation. The publisher error is thrown if the publisher fails before publishing the next element.✅ Otherwise, the next published element is returned.
Example:
Examples of failing tests
recorder.next()
can be inverted:Examples of failing tests
next(count)
recorder.next(count)
waits for the recorded publisher to emitcount
elements, or to complete.RecordingError.notEnoughElements
is thrown if the publisher does not publishcount
elements after last waited expectation. The publisher error is thrown if the publisher fails before publishing the nextcount
elements.✅ Otherwise, an array of exactly
count
elements is returned.Example:
Examples of failing tests
prefix(maxLength)
recorder.prefix(maxLength)
waits for the recorded publisher to emitmaxLength
elements, or to complete.maxLength
elements are published.✅ Otherwise, an array of received elements is returned, containing at most
maxLength
elements, or less if the publisher completes early.Example:
Examples of failing tests
recorder.prefix(maxLength)
can be inverted:Examples of failing tests
recording
recorder.recording
waits for the recorded publisher to complete.RecordingError.notCompleted
is thrown if the publisher does not complete on time.✅ Otherwise, a
Record.Recording
is returned.Example:
Examples of failing tests
single
recorder.single
waits for the recorded publisher to complete.RecordingError
is thrown if the publisher does not complete on time, or does not publish exactly one element before it completes. The publisher error is thrown if the publisher fails.✅ Otherwise, the single published element is returned.
Example:
Examples of failing tests