add swiftUI code
This commit is contained in:
+113
@@ -0,0 +1,113 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
/// A name space for publisher expectations
|
||||
public enum PublisherExpectations { }
|
||||
|
||||
/// The base protocol for PublisherExpectation. It is an implementation detail
|
||||
/// that you are not supposed to use, as shown by the underscore prefix.
|
||||
public protocol _PublisherExpectationBase {
|
||||
/// Sets up an XCTestExpectation. This method is an implementation detail
|
||||
/// that you are not supposed to use, as shown by the underscore prefix.
|
||||
func _setup(_ expectation: XCTestExpectation)
|
||||
|
||||
/// Returns an object that waits for the expectation. If nil, expectation
|
||||
/// is waited by the XCTestCase.
|
||||
func _makeWaiter() -> XCTWaiter?
|
||||
}
|
||||
|
||||
extension _PublisherExpectationBase {
|
||||
public func _makeWaiter() -> XCTWaiter? { nil }
|
||||
}
|
||||
|
||||
/// The protocol for publisher expectations.
|
||||
///
|
||||
/// You can build publisher expectations from Recorder returned by the
|
||||
/// `Publisher.record()` method.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // The expectation for all published elements until completion
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let expectation = recorder.elements
|
||||
///
|
||||
/// When a test grants some time for the expectation to fulfill, use the
|
||||
/// XCTest `wait(for:timeout:description)` method:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let expectation = recorder.elements
|
||||
/// let elements = try wait(for: expectation, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
///
|
||||
/// On the other hand, when the expectation is supposed to be immediately
|
||||
/// fulfilled, use the PublisherExpectation `get()` method in order to grab the
|
||||
/// expected value:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.elements.get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
public protocol PublisherExpectation: _PublisherExpectationBase {
|
||||
/// The type of the expected value.
|
||||
associatedtype Output
|
||||
|
||||
/// Returns the expected value, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.elements.get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
func get() throws -> Output
|
||||
}
|
||||
|
||||
extension XCTestCase {
|
||||
/// Waits for the publisher expectation to fulfill, and returns the
|
||||
/// expected value.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter publisherExpectation: The publisher expectation.
|
||||
/// - parameter timeout: The number of seconds within which the expectation
|
||||
/// must be fulfilled.
|
||||
/// - parameter description: A string to display in the test log for the
|
||||
/// expectation, to help diagnose failures.
|
||||
/// - throws: An error if the expectation fails.
|
||||
public func wait<R: PublisherExpectation>(
|
||||
for publisherExpectation: R,
|
||||
timeout: TimeInterval,
|
||||
description: String = "")
|
||||
throws -> R.Output
|
||||
{
|
||||
let expectation = self.expectation(description: description)
|
||||
publisherExpectation._setup(expectation)
|
||||
if let waiter = publisherExpectation._makeWaiter() {
|
||||
waiter.wait(for: [expectation], timeout: timeout)
|
||||
} else {
|
||||
wait(for: [expectation], timeout: timeout)
|
||||
}
|
||||
return try publisherExpectation.get()
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the timeout to expire, or
|
||||
/// the recorded publisher to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if
|
||||
/// the publisher fails before the expectation has expired.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testTimerPublishesIncreasingDates() throws {
|
||||
/// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||
/// let recorder = publisher.record()
|
||||
/// let dates = try wait(for: recorder.availableElements, timeout: ...)
|
||||
/// XCTAssertEqual(dates.sorted(), dates)
|
||||
/// }
|
||||
public struct AvailableElements<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _makeWaiter() -> XCTWaiter? { Waiter() }
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns all elements published so far, or throws an error if the
|
||||
/// publisher has failed.
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
consume(remainingElements.count)
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
/// A waiter that waits but never fails
|
||||
private class Waiter: XCTWaiter, XCTWaiterDelegate {
|
||||
init() {
|
||||
super.init(delegate: nil)
|
||||
delegate = self
|
||||
}
|
||||
|
||||
func waiter(_ waiter: XCTWaiter, didTimeoutWithUnfulfilledExpectations unfulfilledExpectations: [XCTestExpectation]) { }
|
||||
func waiter(_ waiter: XCTWaiter, fulfillmentDidViolateOrderingConstraintsFor expectation: XCTestExpectation, requiredExpectation: XCTestExpectation) { }
|
||||
func waiter(_ waiter: XCTWaiter, didFulfillInvertedExpectation expectation: XCTestExpectation) { }
|
||||
func nestedWaiter(_ waiter: XCTWaiter, wasInterruptedByTimedOutWaiter outerWaiter: XCTWaiter) { }
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+94
@@ -0,0 +1,94 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
// The Finished expectation waits for the publisher to complete, and throws an
|
||||
// error if and only if the publisher fails with an error.
|
||||
//
|
||||
// It is not derived from the Recording expectation, because Finished does not
|
||||
// throw RecordingError.notCompleted if the publisher does not complete on time.
|
||||
// It only triggers a timeout test failure.
|
||||
//
|
||||
// This allows to write tests for publishers that should not complete:
|
||||
//
|
||||
// // SUCCESS: no timeout, no error
|
||||
// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
// let publisher = PassthroughSubject<String, Never>()
|
||||
// let recorder = publisher.record()
|
||||
// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
// }
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished, timeout: 1)
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public struct Finished<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronouslyFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try recorder.finished.get()
|
||||
/// }
|
||||
public func get() throws {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
guard let completion else {
|
||||
consume(remainingElements.count)
|
||||
return
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for a
|
||||
/// publisher to complete successfully.
|
||||
///
|
||||
/// When waiting for this expectation, an error is thrown if the
|
||||
/// publisher fails with an error.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public var inverted: Inverted<Self> {
|
||||
return Inverted(base: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation that fails if the base expectation is fulfilled.
|
||||
///
|
||||
/// When waiting for this expectation, you receive the same result and
|
||||
/// eventual error as the base expectation.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public struct Inverted<Base: PublisherExpectation>: PublisherExpectation {
|
||||
let base: Base
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
base._setup(expectation)
|
||||
expectation.isInverted.toggle()
|
||||
}
|
||||
|
||||
public func get() throws -> Base.Output {
|
||||
try base.get()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation that transforms the value of a base expectation.
|
||||
///
|
||||
/// This expectation has no public initializer.
|
||||
public struct Map<Base: PublisherExpectation, Output>: PublisherExpectation {
|
||||
let base: Base
|
||||
let transform: (Base.Output) throws -> Output
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
base._setup(expectation)
|
||||
}
|
||||
|
||||
public func get() throws -> Output {
|
||||
try transform(base.get())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectation {
|
||||
/// Returns a publisher expectation that transforms the value of the
|
||||
/// base expectation.
|
||||
func map<T>(_ transform: @escaping (Output) throws -> T) -> PublisherExpectations.Map<Self, T> {
|
||||
PublisherExpectations.Map(base: self, transform: transform)
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+81
@@ -0,0 +1,81 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try wait(for: recorder.next(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try wait(for: recorder.next(1), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
public struct Next<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
let count: Int
|
||||
|
||||
init(recorder: Recorder<Input, Failure>, count: Int) {
|
||||
precondition(count >= 0, "Can't take a prefix of negative length")
|
||||
self.recorder = recorder
|
||||
self.count = count
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if count == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = count
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try recorder.next(2).get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try recorder.next(1).get()
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if remainingElements.count >= count {
|
||||
consume(count)
|
||||
return Array(remainingElements.prefix(count))
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
} else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+123
@@ -0,0 +1,123 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfTwoElementsPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public struct NextOne<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try recorder.next().get()
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try recorder.next().get()
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public func get() throws -> Input {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if let next = remainingElements.first {
|
||||
consume(1)
|
||||
return next
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
} else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for the
|
||||
/// recorded publisher to emit one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError 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 one element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotPublishAnyElement() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.next().inverted, timeout: 1)
|
||||
/// }
|
||||
public var inverted: NextOneInverted<Input, Failure> {
|
||||
return NextOneInverted(recorder: recorder)
|
||||
}
|
||||
}
|
||||
|
||||
/// An inverted publisher expectation which waits for the recorded publisher
|
||||
/// to emit one element, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, a RecordingError 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 one element.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotPublishAnyElement() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.next().inverted, timeout: 1)
|
||||
/// }
|
||||
public struct NextOneInverted<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
expectation.isInverted = true
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: false)
|
||||
}
|
||||
|
||||
public func get() throws {
|
||||
try recorder.value { (_, completion, remainingElements, consume) in
|
||||
if remainingElements.isEmpty == false {
|
||||
return
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
+110
@@ -0,0 +1,110 @@
|
||||
#if canImport(Combine)
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which waits for the recorded publisher to emit
|
||||
/// `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.prefix(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public struct Prefix<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
let maxLength: Int
|
||||
|
||||
init(recorder: Recorder<Input, Failure>, maxLength: Int) {
|
||||
precondition(maxLength >= 0, "Can't take a prefix of negative length")
|
||||
self.recorder = recorder
|
||||
self.maxLength = maxLength
|
||||
}
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
if maxLength == 0 {
|
||||
// Such an expectation is immediately fulfilled, by essence.
|
||||
expectation.expectedFulfillmentCount = 1
|
||||
expectation.fulfill()
|
||||
} else {
|
||||
expectation.expectedFulfillmentCount = maxLength
|
||||
recorder.fulfillOnInput(expectation, includingConsumed: true)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try recorder.prefix(2).get()
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public func get() throws -> [Input] {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if elements.count >= maxLength {
|
||||
let extraCount = max(maxLength + remainingElements.count - elements.count, 0)
|
||||
consume(extraCount)
|
||||
return Array(elements.prefix(maxLength))
|
||||
}
|
||||
if case let .failure(error) = completion {
|
||||
throw error
|
||||
}
|
||||
consume(remainingElements.count)
|
||||
return elements
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns an inverted publisher expectation which waits for a
|
||||
/// publisher to emit `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown
|
||||
/// if the publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
public var inverted: Inverted<Self> {
|
||||
return Inverted(base: self)
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
+62
@@ -0,0 +1,62 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import XCTest
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// A publisher expectation which 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.
|
||||
///
|
||||
/// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try wait(for: recorder.recording, timeout: 1)
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public struct Recording<Input, Failure: Error>: PublisherExpectation {
|
||||
let recorder: Recorder<Input, Failure>
|
||||
|
||||
public func _setup(_ expectation: XCTestExpectation) {
|
||||
recorder.fulfillOnCompletion(expectation)
|
||||
}
|
||||
|
||||
/// Returns the expected output, or throws an error if the
|
||||
/// expectation fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no error
|
||||
/// func testArrayPublisherSynchronousRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try recorder.recording.get()
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public func get() throws -> Record<Input, Failure>.Recording {
|
||||
try recorder.value { (elements, completion, remainingElements, consume) in
|
||||
if let completion {
|
||||
consume(remainingElements.count)
|
||||
return Record<Input, Failure>.Recording(output: elements, completion: completion)
|
||||
} else {
|
||||
throw RecordingError.notCompleted
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -0,0 +1,617 @@
|
||||
#if canImport(Combine)
|
||||
import Combine
|
||||
import XCTest
|
||||
|
||||
/// A Combine subscriber which records all events published by a publisher.
|
||||
///
|
||||
/// You create a Recorder with the `Publisher.record()` method:
|
||||
///
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// You can build publisher expectations from the Recorder. For example:
|
||||
///
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
public class Recorder<Input, Failure: Error>: Subscriber {
|
||||
public typealias Input = Input
|
||||
public typealias Failure = Failure
|
||||
|
||||
private enum RecorderExpectation {
|
||||
case onInput(XCTestExpectation, remainingCount: Int)
|
||||
case onCompletion(XCTestExpectation)
|
||||
|
||||
var expectation: XCTestExpectation {
|
||||
switch self {
|
||||
case let .onCompletion(expectation):
|
||||
return expectation
|
||||
case let .onInput(expectation, remainingCount: _):
|
||||
return expectation
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The recorder state
|
||||
private enum State {
|
||||
/// Publisher is not subscribed yet. The recorder may have an
|
||||
/// expectation to fulfill.
|
||||
case waitingForSubscription(RecorderExpectation?)
|
||||
|
||||
/// Publisher is subscribed. The recorder may have an expectation to
|
||||
/// fulfill. It keeps track of all published elements.
|
||||
case subscribed(Subscription, RecorderExpectation?, [Input])
|
||||
|
||||
/// Publisher is completed. The recorder keeps track of all published
|
||||
/// elements and completion.
|
||||
case completed([Input], Subscribers.Completion<Failure>)
|
||||
|
||||
var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion<Failure>?) {
|
||||
switch self {
|
||||
case .waitingForSubscription:
|
||||
return (elements: [], completion: nil)
|
||||
case let .subscribed(_, _, elements):
|
||||
return (elements: elements, completion: nil)
|
||||
case let .completed(elements, completion):
|
||||
return (elements: elements, completion: completion)
|
||||
}
|
||||
}
|
||||
|
||||
var recorderExpectation: RecorderExpectation? {
|
||||
switch self {
|
||||
case let .waitingForSubscription(exp), let .subscribed(_, exp, _):
|
||||
return exp
|
||||
case .completed:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private let lock = NSLock()
|
||||
private var state = State.waitingForSubscription(nil)
|
||||
private var consumedCount = 0
|
||||
|
||||
/// The elements and completion recorded so far.
|
||||
var elementsAndCompletion: (elements: [Input], completion: Subscribers.Completion<Failure>?) {
|
||||
synchronized {
|
||||
state.elementsAndCompletion
|
||||
}
|
||||
}
|
||||
|
||||
/// Use Publisher.record()
|
||||
fileprivate init() { }
|
||||
|
||||
deinit {
|
||||
if case let .subscribed(subscription, _, _) = state {
|
||||
subscription.cancel()
|
||||
}
|
||||
}
|
||||
|
||||
private func synchronized<T>(_ execute: () throws -> T) rethrows -> T {
|
||||
lock.lock()
|
||||
defer { lock.unlock() }
|
||||
return try execute()
|
||||
}
|
||||
|
||||
// MARK: - PublisherExpectation API
|
||||
|
||||
/// Registers the expectation so that it gets fulfilled when publisher
|
||||
/// publishes elements or completes.
|
||||
///
|
||||
/// - parameter expectation: An XCTestExpectation.
|
||||
/// - parameter includingConsumed: This flag controls how elements that were
|
||||
/// already published at the time this method is called fulfill the
|
||||
/// expectation. If true, all published elements fulfill the expectation.
|
||||
/// If false, only published elements that are not consumed yet fulfill
|
||||
/// the expectation. For example, the Prefix expectation uses true, but
|
||||
/// the NextOne expectation uses false.
|
||||
func fulfillOnInput(_ expectation: XCTestExpectation, includingConsumed: Bool) {
|
||||
lock.lock()
|
||||
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
let expectedFulfillmentCount = expectation.expectedFulfillmentCount
|
||||
|
||||
switch state {
|
||||
case .waitingForSubscription:
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: expectedFulfillmentCount)
|
||||
state = .waitingForSubscription(exp)
|
||||
lock.unlock()
|
||||
|
||||
case let .subscribed(subscription, _, elements):
|
||||
let maxFulfillmentCount = includingConsumed
|
||||
? elements.count
|
||||
: elements.count - consumedCount
|
||||
let fulfillmentCount = min(expectedFulfillmentCount, maxFulfillmentCount)
|
||||
|
||||
let remainingCount = expectedFulfillmentCount - fulfillmentCount
|
||||
if remainingCount > 0 {
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
}
|
||||
lock.unlock()
|
||||
expectation.fulfill(count: fulfillmentCount)
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
expectation.fulfill(count: expectedFulfillmentCount)
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers the expectation so that it gets fulfilled when
|
||||
/// publisher completes.
|
||||
func fulfillOnCompletion(_ expectation: XCTestExpectation) {
|
||||
lock.lock()
|
||||
|
||||
preconditionCanFulfillExpectation()
|
||||
|
||||
switch state {
|
||||
case .waitingForSubscription:
|
||||
let exp = RecorderExpectation.onCompletion(expectation)
|
||||
state = .waitingForSubscription(exp)
|
||||
lock.unlock()
|
||||
|
||||
case let .subscribed(subscription, _, elements):
|
||||
let exp = RecorderExpectation.onCompletion(expectation)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
lock.unlock()
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
expectation.fulfill()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a value based on the recorded state of the publisher.
|
||||
///
|
||||
/// - parameter value: A function which returns the value, given the
|
||||
/// recorded state of the publisher.
|
||||
/// - parameter elements: All recorded elements.
|
||||
/// - parameter completion: The eventual publisher completion.
|
||||
/// - parameter remainingElements: The elements that were not consumed yet.
|
||||
/// - parameter consume: A function which consumes elements.
|
||||
/// - parameter count: The number of consumed elements.
|
||||
/// - returns: The value
|
||||
func value<T>(_ value: (
|
||||
_ elements: [Input],
|
||||
_ completion: Subscribers.Completion<Failure>?,
|
||||
_ remainingElements: ArraySlice<Input>,
|
||||
_ consume: (_ count: Int) -> ()) throws -> T)
|
||||
rethrows -> T
|
||||
{
|
||||
try synchronized {
|
||||
let (elements, completion) = state.elementsAndCompletion
|
||||
let remainingElements = elements[consumedCount...]
|
||||
return try value(elements, completion, remainingElements, { count in
|
||||
precondition(count >= 0)
|
||||
precondition(count <= remainingElements.count)
|
||||
consumedCount += count
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// Checks that recorder can fulfill an expectation.
|
||||
///
|
||||
/// The reason this method exists is that a recorder can fulfill a single
|
||||
/// expectation at a given time. It is a programmer error to wait for two
|
||||
/// expectations concurrently.
|
||||
///
|
||||
/// This method MUST be called within a synchronized block.
|
||||
private func preconditionCanFulfillExpectation() {
|
||||
if let exp = state.recorderExpectation {
|
||||
// We are already waiting for an expectation! Is it a programmer
|
||||
// error? Recorder drops references to non-inverted expectations
|
||||
// when they are fulfilled. But inverted expectations are not
|
||||
// fulfilled, and thus not dropped. We can't quite know if an
|
||||
// inverted expectations has expired yet, so just let it go.
|
||||
precondition(exp.expectation.isInverted, "Already waiting for an expectation")
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Subscriber
|
||||
|
||||
public func receive(subscription: Subscription) {
|
||||
synchronized {
|
||||
switch state {
|
||||
case let .waitingForSubscription(exp):
|
||||
state = .subscribed(subscription, exp, [])
|
||||
default:
|
||||
XCTFail("Publisher recorder is already subscribed")
|
||||
}
|
||||
}
|
||||
subscription.request(.unlimited)
|
||||
}
|
||||
|
||||
public func receive(_ input: Input) -> Subscribers.Demand {
|
||||
lock.lock()
|
||||
|
||||
switch state {
|
||||
case let .subscribed(subscription, exp, elements):
|
||||
var elements = elements
|
||||
elements.append(input)
|
||||
|
||||
if case let .onInput(expectation, remainingCount: remainingCount) = exp {
|
||||
assert(remainingCount > 0)
|
||||
expectation.fulfill()
|
||||
if remainingCount > 1 {
|
||||
let exp = RecorderExpectation.onInput(expectation, remainingCount: remainingCount - 1)
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
} else {
|
||||
state = .subscribed(subscription, nil, elements)
|
||||
}
|
||||
} else {
|
||||
state = .subscribed(subscription, exp, elements)
|
||||
}
|
||||
|
||||
lock.unlock()
|
||||
return .unlimited
|
||||
|
||||
case .waitingForSubscription:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected input before subscription: \(String(reflecting: input))")
|
||||
return .none
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected input after completion: \(String(reflecting: input))")
|
||||
return .none
|
||||
}
|
||||
}
|
||||
|
||||
public func receive(completion: Subscribers.Completion<Failure>) {
|
||||
lock.lock()
|
||||
|
||||
switch state {
|
||||
case let .subscribed(_, exp, elements):
|
||||
if let exp {
|
||||
switch exp {
|
||||
case let .onCompletion(expectation):
|
||||
expectation.fulfill()
|
||||
case let .onInput(expectation, remainingCount: remainingCount):
|
||||
expectation.fulfill(count: remainingCount)
|
||||
}
|
||||
}
|
||||
state = .completed(elements, completion)
|
||||
lock.unlock()
|
||||
|
||||
case .waitingForSubscription:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected completion before subscription: \(String(describing: completion))")
|
||||
|
||||
case .completed:
|
||||
lock.unlock()
|
||||
XCTFail("Publisher recorder got unexpected completion after completion: \(String(describing: completion))")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Publisher Expectations
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension PublisherExpectations {
|
||||
/// The type of the publisher expectation returned by `Recorder.completion`.
|
||||
public typealias Completion<Input, Failure: Error> = Map<Recording<Input, Failure>, Subscribers.Completion<Failure>>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.elements`.
|
||||
public typealias Elements<Input, Failure: Error> = Map<Recording<Input, Failure>, [Input]>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.last`.
|
||||
public typealias Last<Input, Failure: Error> = Map<Elements<Input, Failure>, Input?>
|
||||
|
||||
/// The type of the publisher expectation returned by `Recorder.single`.
|
||||
public typealias Single<Input, Failure: Error> = Map<Elements<Input, Failure>, Input>
|
||||
}
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension Recorder {
|
||||
/// Returns a publisher expectation which waits for the timeout to expire,
|
||||
/// or the recorded publisher to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if
|
||||
/// the publisher fails before the expectation has expired.
|
||||
///
|
||||
/// 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testTimerPublishesIncreasingDates() throws {
|
||||
/// let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
|
||||
/// let recorder = publisher.record()
|
||||
/// let dates = try wait(for: recorder.availableElements, timeout: ...)
|
||||
/// XCTAssertEqual(dates.sorted(), dates)
|
||||
/// }
|
||||
public var availableElements: PublisherExpectations.AvailableElements<Input, Failure> {
|
||||
PublisherExpectations.AvailableElements(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// Otherwise, a [Subscribers.Completion](https://developer.apple.com/documentation/combine/subscribers/completion)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // 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: 1)
|
||||
/// if case let .failure(error) = completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public var completion: PublisherExpectations.Completion<Input, Failure> {
|
||||
recording.map { $0.completion }
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherPublishesArrayElements() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
/// }
|
||||
public var elements: PublisherExpectations.Elements<Input, Failure> {
|
||||
recording.map { recording in
|
||||
if case let .failure(error) = recording.completion {
|
||||
throw error
|
||||
}
|
||||
return recording.output
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherFinishesWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished, timeout: 1)
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectDoesNotFinish() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// try wait(for: recorder.finished.inverted, timeout: 1)
|
||||
/// }
|
||||
public var finished: PublisherExpectations.Finished<Input, Failure> {
|
||||
PublisherExpectations.Finished(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // 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: 1) {
|
||||
/// XCTAssertEqual(element, "baz")
|
||||
/// } else {
|
||||
/// XCTFail("Expected one element")
|
||||
/// }
|
||||
/// }
|
||||
public var last: PublisherExpectations.Last<Input, Failure> {
|
||||
elements.map { $0.last }
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfTwoElementsPublishesElementsInOrder() throws {
|
||||
/// let publisher = ["foo", "bar"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
///
|
||||
/// element = try wait(for: recorder.next(), timeout: 1)
|
||||
/// XCTAssertEqual(element, "bar")
|
||||
/// }
|
||||
public func next() -> PublisherExpectations.NextOne<Input, Failure> {
|
||||
PublisherExpectations.NextOne(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoThenOneElement() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// var elements = try wait(for: recorder.next(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
///
|
||||
/// elements = try wait(for: recorder.next(1), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["baz"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter count: The number of elements.
|
||||
public func next(_ count: Int) -> PublisherExpectations.Next<Input, Failure> {
|
||||
PublisherExpectations.Next(recorder: self, count: count)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which waits for the recorded publisher
|
||||
/// to emit `maxLength` elements, or to complete.
|
||||
///
|
||||
/// When waiting for this expectation, the publisher error is thrown if the
|
||||
/// publisher fails before `maxLength` elements are published.
|
||||
///
|
||||
/// Otherwise, an array of received elements is returned, containing at
|
||||
/// most `maxLength` elements, or less if the publisher completes early.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let elements = try wait(for: recorder.prefix(2), timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// This publisher expectation can be inverted:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
|
||||
/// let publisher = PassthroughSubject<String, Never>()
|
||||
/// let recorder = publisher.record()
|
||||
/// publisher.send("foo")
|
||||
/// publisher.send("bar")
|
||||
/// let elements = try wait(for: recorder.prefix(3).inverted, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar"])
|
||||
/// }
|
||||
///
|
||||
/// - parameter maxLength: The maximum number of elements.
|
||||
public func prefix(_ maxLength: Int) -> PublisherExpectations.Prefix<Input, Failure> {
|
||||
PublisherExpectations.Prefix(recorder: self, maxLength: maxLength)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// Otherwise, a [Record.Recording](https://developer.apple.com/documentation/combine/record/recording)
|
||||
/// is returned.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testArrayPublisherRecording() throws {
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
/// let recording = try wait(for: recorder.recording, timeout: 1)
|
||||
/// XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
|
||||
/// if case let .failure(error) = recording.completion {
|
||||
/// XCTFail("Unexpected error \(error)")
|
||||
/// }
|
||||
/// }
|
||||
public var recording: PublisherExpectations.Recording<Input, Failure> {
|
||||
PublisherExpectations.Recording(recorder: self)
|
||||
}
|
||||
|
||||
/// Returns a publisher expectation which 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.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// // SUCCESS: no timeout, no error
|
||||
/// func testJustPublishesExactlyOneElement() throws {
|
||||
/// let publisher = Just("foo")
|
||||
/// let recorder = publisher.record()
|
||||
/// let element = try wait(for: recorder.single, timeout: 1)
|
||||
/// XCTAssertEqual(element, "foo")
|
||||
/// }
|
||||
public var single: PublisherExpectations.Single<Input, Failure> {
|
||||
elements.map { elements in
|
||||
guard let element = elements.first else {
|
||||
throw RecordingError.notEnoughElements
|
||||
}
|
||||
if elements.count > 1 {
|
||||
throw RecordingError.tooManyElements
|
||||
}
|
||||
return element
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Publisher + Recorder
|
||||
|
||||
@available(iOS 13, macOS 10.15, tvOS 13, watchOS 6, *)
|
||||
extension Publisher {
|
||||
/// Returns a subscribed Recorder.
|
||||
///
|
||||
/// For example:
|
||||
///
|
||||
/// let publisher = ["foo", "bar", "baz"].publisher
|
||||
/// let recorder = publisher.record()
|
||||
///
|
||||
/// You can build publisher expectations from the Recorder. For example:
|
||||
///
|
||||
/// let elements = try wait(for: recorder.elements, timeout: 1)
|
||||
/// XCTAssertEqual(elements, ["foo", "bar", "baz"])
|
||||
public func record() -> Recorder<Output, Failure> {
|
||||
let recorder = Recorder<Output, Failure>()
|
||||
subscribe(recorder)
|
||||
return recorder
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Convenience
|
||||
|
||||
extension XCTestExpectation {
|
||||
fileprivate func fulfill(count: Int) {
|
||||
for _ in 0..<count {
|
||||
fulfill()
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
#if canImport(Combine)
|
||||
import Foundation
|
||||
|
||||
/// An error that may be thrown when waiting for publisher expectations.
|
||||
public enum RecordingError: Error {
|
||||
/// The publisher did not complete.
|
||||
case notCompleted
|
||||
|
||||
/// The publisher did not publish enough elements.
|
||||
/// For example, see `recorder.single`.
|
||||
case notEnoughElements
|
||||
|
||||
/// The publisher did publish too many elements.
|
||||
/// For example, see `recorder.single`.
|
||||
case tooManyElements
|
||||
}
|
||||
|
||||
extension RecordingError: LocalizedError {
|
||||
public var errorDescription: String? {
|
||||
switch self {
|
||||
case .notCompleted:
|
||||
return "RecordingError.notCompleted"
|
||||
case .notEnoughElements:
|
||||
return "RecordingError.notEnoughElements"
|
||||
case .tooManyElements:
|
||||
return "RecordingError.tooManyElements"
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
Reference in New Issue
Block a user