Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Recorder.availableElements #11

Merged
merged 4 commits into from
Dec 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,9 @@ All notable changes to this project will be documented in this file.
- [0.2.0](#020)
- [0.1.0](#010)

## Development Branch

- **New**: `availableElements` expectation

## 0.5.0

Expand Down
33 changes: 29 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ CombineExpectations aims at streamlining those tests. It defines an XCTestCase m

- [Usage]
- [Installation]
- [Publisher Expectations]: [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single]
- [Publisher Expectations]: [availableElements], [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single]

---

Expand Down Expand Up @@ -112,7 +112,6 @@ class PublisherTests: XCTestCase {
```



## Installation

Add a dependency for CombineExpectations to your [Swift Package](https://swift.org/package-manager/) test targets:
Expand All @@ -138,6 +137,7 @@ Add a dependency for CombineExpectations to your [Swift Package](https://swift.o

There are various publisher expectations. Each one waits for a specific publisher aspect:

- [availableElements]: all published elements until timeout expiration
- [completion]: the publisher completion
- [elements]: all published elements until successful completion
- [finished]: the publisher successful completion
Expand All @@ -150,6 +150,30 @@ There are various publisher expectations. Each one waits for a specific publishe

---

### availableElements

:clock230: `recorder.availableElements` waits for the expectation to expire, or the recorded publisher to complete.

:x: When waiting for this expectation, the publisher error is thrown if the publisher fails before the expectation has expired.

:white_check_mark: Otherwise, an array of all elements published before the expectation has expired is returned.

:arrow_right: Related expectations: [elements], [prefix(maxLength)].

Unlike other expectations, `availableElements` does not make a test fail on timeout expiration. It just returns the elements published so far.

Example:

```swift
// 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)
}
```

### completion

:clock230: `recorder.completion` waits for the recorded publisher to complete.
Expand Down Expand Up @@ -210,7 +234,7 @@ func testCompletionTimeout() throws {

:white_check_mark: Otherwise, an array of published elements is returned.

:arrow_right: Related expectations: [last], [prefix(maxLength)], [recording], [single].
:arrow_right: Related expectations: [availableElements], [last], [prefix(maxLength)], [recording], [single].

Example:

Expand Down Expand Up @@ -588,7 +612,7 @@ func testNextCountNotEnoughElementsError() throws {

:white_check_mark: Otherwise, an array of received elements is returned, containing at most `maxLength` elements, or less if the publisher completes early.

:arrow_right: Related expectations: [elements], [next(count)].
:arrow_right: Related expectations: [availableElements], [elements], [next(count)].

Example:

Expand Down Expand Up @@ -817,3 +841,4 @@ func testSingleNotEnoughElementsError() throws {
[elements]: #elements
[last]: #last
[single]: #single
[availableElements]: #availableElements
17 changes: 16 additions & 1 deletion Sources/CombineExpectations/PublisherExpectation.swift
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,21 @@ 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.
///
/// :nodoc:
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 {
/// :nodoc:
public func _makeWaiter() -> XCTWaiter? { nil }
}

/// The protocol for publisher expectations.
Expand Down Expand Up @@ -93,7 +104,11 @@ extension XCTestCase {
{
let expectation = self.expectation(description: description)
publisherExpectation._setup(expectation)
wait(for: [expectation], timeout: timeout)
if let waiter = publisherExpectation._makeWaiter() {
waiter.wait(for: [expectation], timeout: timeout)
} else {
wait(for: [expectation], timeout: timeout)
}
return try publisherExpectation.get()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import XCTest

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) { }
}
}
}
33 changes: 29 additions & 4 deletions Sources/CombineExpectations/Recorder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -274,20 +274,45 @@ public class Recorder<Input, Failure: Error>: Subscriber {
// MARK: - Publisher Expectations

extension PublisherExpectations {
/// The type of the publisher expectation returned by Recorder.completion
/// 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
/// 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
/// 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
/// The type of the publisher expectation returned by `Recorder.single`.
public typealias Single<Input, Failure: Error> = Map<Elements<Input, Failure>, Input>
}

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.
///
Expand Down
53 changes: 53 additions & 0 deletions Tests/CombineExpectationsTests/RecorderTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,59 @@ class RecorderTests: XCTestCase {
XCTAssertTrue(subscribed)
}

// MARK: - availableElements

func testAvailableElementsSync() throws {
do {
let publisher = [1, 2, 3].publisher
let recorder = publisher.record()
let availableElements = try recorder.availableElements.get()
XCTAssertEqual(availableElements, [1, 2, 3])
}
do {
let publisher = PassthroughSubject<Int, Never>()
let recorder = publisher.record()
publisher.send(1)
publisher.send(2)
publisher.send(3)
let availableElements = try recorder.availableElements.get()
XCTAssertEqual(availableElements, [1, 2, 3])
}
do {
let publisher = PassthroughSubject<Int, TestError>()
let recorder = publisher.record()
publisher.send(1)
publisher.send(completion: .failure(TestError()))
_ = try recorder.availableElements.get()
XCTFail("Expected TestError")
} catch is TestError { }
}

func testAvailableElementsAsync() throws {
do {
let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
let recorder = publisher.record()
let dates = try wait(for: recorder.availableElements, timeout: 0.1)
XCTAssertTrue(dates.count > 2)
XCTAssertEqual(dates.sorted(), dates)
}
}

func testAvailableElementsStopsOnPublisherCompletion() throws {
do {
let publisher = PassthroughSubject<Int, TestError>()
let recorder = publisher.record()

DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) {
publisher.send(completion: .finished)
}
let start = Date()
_ = try wait(for: recorder.availableElements, timeout: 2)
let duration = Date().timeIntervalSince(start)
XCTAssertLessThan(duration, 1)
}
}

// MARK: - elementsAndCompletion

func testElementsAndCompletionSync() throws {
Expand Down