项目作者: groue

项目描述 :
Utilities for tests that wait for Combine publishers
高级语言: Swift
项目地址: git://github.com/groue/CombineExpectations.git
创建时间: 2019-11-23T13:22:40Z
项目社区:https://github.com/groue/CombineExpectations

开源协议:MIT License

下载


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:

  1. import XCTest
  2. import CombineExpectations
  3. class PublisherTests: XCTestCase {
  4. func testElements() throws {
  5. // 1. Create a publisher
  6. let publisher = ...
  7. // 2. Start recording the publisher
  8. let recorder = publisher.record()
  9. // 3. Wait for a publisher expectation
  10. let elements = try wait(for: recorder.elements, timeout: ..., description: "Elements")
  11. // 4. Test the result of the expectation
  12. XCTAssertEqual(elements, ["Hello", "World!"])
  13. }
  14. }

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:

  1. class PublisherTests: XCTestCase {
  2. func testPublisher() throws {
  3. let publisher = ...
  4. let recorder = publisher.record()
  5. // Wait for first element
  6. _ = try wait(for: recorder.next(), timeout: ...)
  7. // Wait for second element
  8. _ = try wait(for: recorder.next(), timeout: ...)
  9. // Wait for successful completion
  10. try wait(for: recorder.finished, timeout: ...)
  11. }
  12. }

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:

  1. class PublisherTests: XCTestCase {
  2. func testSynchronousPublisher() throws {
  3. // 1. Create a publisher
  4. let publisher = ...
  5. // 2. Start recording the publisher
  6. let recorder = publisher.record()
  7. // 3. Grab the expected result
  8. let elements = try recorder.elements.get()
  9. // 4. Test the result of the expectation
  10. XCTAssertEqual(elements, ["Hello", "World!"])
  11. }
  12. }

Just like wait(for:timeout:), the get() method can be called multiple times:

  1. class PublisherTests: XCTestCase {
  2. // SUCCESS: no error
  3. func testPassthroughSubjectSynchronouslyPublishesElements() throws {
  4. let publisher = PassthroughSubject<String, Never>()
  5. let recorder = publisher.record()
  6. publisher.send("foo")
  7. try XCTAssertEqual(recorder.next().get(), "foo")
  8. publisher.send("bar")
  9. try XCTAssertEqual(recorder.next().get(), "bar")
  10. }
  11. }

Installation

Add a dependency for CombineExpectations to your Swift Package test targets:

  1. import PackageDescription
  2. let package = Package(
  3. dependencies: [
  4. + .package(url: "https://github.com/groue/CombineExpectations.git", ...)
  5. ],
  6. targets: [
  7. .testTarget(
  8. dependencies: [
  9. + "CombineExpectations"
  10. ])
  11. ]
  12. )

Publisher Expectations

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


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:

  1. // SUCCESS: no timeout, no error
  2. func testTimerPublishesIncreasingDates() throws {
  3. let publisher = Timer.publish(every: 0.01, on: .main, in: .common).autoconnect()
  4. let recorder = publisher.record()
  5. let dates = try wait(for: recorder.availableElements, timeout: ...)
  6. XCTAssertEqual(dates.sorted(), dates)
  7. }

completion

:clock230: recorder.completion waits for the recorded publisher to complete.

:x: When waiting for this expectation, a RecordingError.notCompleted is thrown if the publisher does not complete on time.

:white_check_mark: Otherwise, a Subscribers.Completion is returned.

:arrow_right: Related expectations: finished, recording.

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayPublisherCompletesWithSuccess() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. let completion = try wait(for: recorder.completion, timeout: ...)
  6. if case let .failure(error) = completion {
  7. XCTFail("Unexpected error \(error)")
  8. }
  9. }
  10. // SUCCESS: no error
  11. func testArrayPublisherSynchronouslyCompletesWithSuccess() throws {
  12. let publisher = ["foo", "bar", "baz"].publisher
  13. let recorder = publisher.record()
  14. let completion = try recorder.completion.get()
  15. if case let .failure(error) = completion {
  16. XCTFail("Unexpected error \(error)")
  17. }
  18. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testCompletionTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() let completion = try wait(for: recorder.completion, timeout: ...) }


elements

:clock230: recorder.elements waits for the recorded publisher to complete.

:x: 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.

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

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

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayPublisherPublishesArrayElements() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. let elements = try wait(for: recorder.elements, timeout: ...)
  6. XCTAssertEqual(elements, ["foo", "bar", "baz"])
  7. }
  8. // SUCCESS: no error
  9. func testArrayPublisherSynchronouslyPublishesArrayElements() throws {
  10. let publisher = ["foo", "bar", "baz"].publisher
  11. let recorder = publisher.record()
  12. let elements = try recorder.elements.get()
  13. XCTAssertEqual(elements, ["foo", "bar", "baz"])
  14. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testElementsTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() let elements = try wait(for: recorder.elements, timeout: ...) } // FAIL: Caught error MyError func testElementsError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.elements, timeout: ...) }


finished

:clock230: recorder.finished waits for the recorded publisher to complete.

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

:arrow_right: Related expectations: completion, recording.

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayPublisherFinishesWithoutError() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. try wait(for: recorder.finished, timeout: ...)
  6. }
  7. // SUCCESS: no error
  8. func testArrayPublisherSynchronouslyFinishesWithoutError() throws {
  9. let publisher = ["foo", "bar", "baz"].publisher
  10. let recorder = publisher.record()
  11. try recorder.finished.get()
  12. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed func testFinishedTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() try wait(for: recorder.finished, timeout: ...) } // FAIL: Caught error MyError func testFinishedError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished, timeout: ...) }

recorder.finished can be inverted:

  1. // SUCCESS: no timeout, no error
  2. func testPassthroughSubjectDoesNotFinish() throws {
  3. let publisher = PassthroughSubject<String, Never>()
  4. let recorder = publisher.record()
  5. try wait(for: recorder.finished.inverted, timeout: ...)
  6. }

Examples of failing tests

swift // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedFinishedError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.finished.inverted, timeout: ...) }


last

:clock230: recorder.last waits for the recorded publisher to complete.

:x: 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.

:white_check_mark: Otherwise, the last published element is returned, or nil if the publisher completes before it publishes any element.

:arrow_right: Related expectations: elements, single.

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayPublisherPublishesLastElementLast() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. if let element = try wait(for: recorder.last, timeout: ...) {
  6. XCTAssertEqual(element, "baz")
  7. } else {
  8. XCTFail("Expected one element")
  9. }
  10. }
  11. // SUCCESS: no error
  12. func testArrayPublisherSynchronouslyPublishesLastElementLast() throws {
  13. let publisher = ["foo", "bar", "baz"].publisher
  14. let recorder = publisher.record()
  15. if let element = try recorder.last.get() {
  16. XCTAssertEqual(element, "baz")
  17. } else {
  18. XCTFail("Expected one element")
  19. }
  20. }

Examples of failing tests

swift // 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()

:clock230: recorder.next() waits for the recorded publisher to emit one element, or to complete.

:x: 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.

:white_check_mark: Otherwise, the next published element is returned.

:arrow_right: Related expectations: next(count), single.

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayOfTwoElementsPublishesElementsInOrder() throws {
  3. let publisher = ["foo", "bar"].publisher
  4. let recorder = publisher.record()
  5. var element = try wait(for: recorder.next(), timeout: ...)
  6. XCTAssertEqual(element, "foo")
  7. element = try wait(for: recorder.next(), timeout: ...)
  8. XCTAssertEqual(element, "bar")
  9. }
  10. // SUCCESS: no error
  11. func testArrayOfTwoElementsSynchronouslyPublishesElementsInOrder() throws {
  12. let publisher = ["foo", "bar"].publisher
  13. let recorder = publisher.record()
  14. var element = try recorder.next().get()
  15. XCTAssertEqual(element, "foo")
  16. element = try recorder.next().get()
  17. XCTAssertEqual(element, "bar")
  18. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() let element = try wait(for: recorder.next(), timeout: ...) } // FAIL: Caught error MyError func testNextError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) let element = try wait(for: recorder.next(), timeout: ...) } // FAIL: Caught error RecordingError.notEnoughElements func testNextNotEnoughElementsError() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send(completion: .finished) let element = try wait(for: recorder.next(), timeout: ...) }

recorder.next() can be inverted:

  1. // SUCCESS: no timeout, no error
  2. func testPassthroughSubjectDoesNotPublishAnyElement() throws {
  3. let publisher = PassthroughSubject<String, Never>()
  4. let recorder = publisher.record()
  5. try wait(for: recorder.next().inverted, timeout: ...)
  6. }

Examples of failing tests

swift // FAIL: Fulfilled inverted expectation func testInvertedNextTooEarly() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send("foo") try wait(for: recorder.next().inverted, timeout: ...) } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedNextError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send(completion: .failure(MyError())) try wait(for: recorder.next().inverted, timeout: ...) }


next(count)

:clock230: recorder.next(count) waits for the recorded publisher to emit count elements, or to complete.

:x: 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.

:white_check_mark: Otherwise, an array of exactly count elements is returned.

:arrow_right: Related expectations: next(), prefix(maxLength).

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayOfThreeElementsPublishesTwoThenOneElement() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. var elements = try wait(for: recorder.next(2), timeout: ...)
  6. XCTAssertEqual(elements, ["foo", "bar"])
  7. elements = try wait(for: recorder.next(1), timeout: ...)
  8. XCTAssertEqual(elements, ["baz"])
  9. }
  10. // SUCCESS: no error
  11. func testArrayOfThreeElementsSynchronouslyPublishesTwoThenOneElement() throws {
  12. let publisher = ["foo", "bar", "baz"].publisher
  13. let recorder = publisher.record()
  14. var elements = try recorder.next(2).get()
  15. XCTAssertEqual(elements, ["foo", "bar"])
  16. elements = try recorder.next(1).get()
  17. XCTAssertEqual(elements, ["baz"])
  18. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notEnoughElements func testNextCountTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send("foo") let elements = try wait(for: recorder.next(2), timeout: ...) } // FAIL: Caught error MyError func testNextCountError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.next(2), timeout: ...) } // FAIL: Caught error RecordingError.notEnoughElements func testNextCountNotEnoughElementsError() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .finished) let elements = try wait(for: recorder.next(2), timeout: ...) }


prefix(maxLength)

:clock230: recorder.prefix(maxLength) waits for the recorded publisher to emit maxLength elements, or to complete.

:x: When waiting for this expectation, the publisher error is thrown if the publisher fails before maxLength elements are published.

: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: availableElements, elements, next(count).

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayOfThreeElementsPublishesTwoFirstElementsWithoutError() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. let elements = try wait(for: recorder.prefix(2), timeout: ...)
  6. XCTAssertEqual(elements, ["foo", "bar"])
  7. }
  8. // SUCCESS: no error
  9. func testArrayOfThreeElementsSynchronouslyPublishesTwoFirstElementsWithoutError() throws {
  10. let publisher = ["foo", "bar", "baz"].publisher
  11. let recorder = publisher.record()
  12. let elements = try recorder.prefix(2).get()
  13. XCTAssertEqual(elements, ["foo", "bar"])
  14. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed func testPrefixTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send("foo") let elements = try wait(for: recorder.prefix(2), timeout: ...) } // FAIL: Caught error MyError func testPrefixError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.prefix(2), timeout: ...) }

recorder.prefix(maxLength) can be inverted:

  1. // SUCCESS: no timeout, no error
  2. func testPassthroughSubjectPublishesNoMoreThanSentValues() throws {
  3. let publisher = PassthroughSubject<String, Never>()
  4. let recorder = publisher.record()
  5. publisher.send("foo")
  6. publisher.send("bar")
  7. let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...)
  8. XCTAssertEqual(elements, ["foo", "bar"])
  9. }

Examples of failing tests

swift // FAIL: Fulfilled inverted expectation func testInvertedPrefixTooEarly() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() publisher.send("foo") publisher.send("bar") publisher.send("baz") let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) } // FAIL: Fulfilled inverted expectation // FAIL: Caught error MyError func testInvertedPrefixError() throws { let publisher = PassthroughSubject<String, MyError>() let recorder = publisher.record() publisher.send("foo") publisher.send(completion: .failure(MyError())) let elements = try wait(for: recorder.prefix(3).inverted, timeout: ...) }


recording

:clock230: recorder.recording waits for the recorded publisher to complete.

:x: When waiting for this expectation, a RecordingError.notCompleted is thrown if the publisher does not complete on time.

:white_check_mark: Otherwise, a Record.Recording is returned.

:arrow_right: Related expectations: completion, elements, finished.

Example:

  1. // SUCCESS: no timeout, no error
  2. func testArrayPublisherRecording() throws {
  3. let publisher = ["foo", "bar", "baz"].publisher
  4. let recorder = publisher.record()
  5. let recording = try wait(for: recorder.recording, timeout: ...)
  6. XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
  7. if case let .failure(error) = recording.completion {
  8. XCTFail("Unexpected error \(error)")
  9. }
  10. }
  11. // SUCCESS: no error
  12. func testArrayPublisherSynchronousRecording() throws {
  13. let publisher = ["foo", "bar", "baz"].publisher
  14. let recorder = publisher.record()
  15. let recording = try recorder.recording.get()
  16. XCTAssertEqual(recording.output, ["foo", "bar", "baz"])
  17. if case let .failure(error) = recording.completion {
  18. XCTFail("Unexpected error \(error)")
  19. }
  20. }

Examples of failing tests

swift // FAIL: Asynchronous wait failed // FAIL: Caught error RecordingError.notCompleted func testRecordingTimeout() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() let recording = try wait(for: recorder.recording, timeout: ...) }


single

:clock230: recorder.single waits for the recorded publisher to complete.

:x: 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.

:white_check_mark: Otherwise, the single published element is returned.

:arrow_right: Related expectations: elements, last, next().

Example:

  1. // SUCCESS: no timeout, no error
  2. func testJustPublishesExactlyOneElement() throws {
  3. let publisher = Just("foo")
  4. let recorder = publisher.record()
  5. let element = try wait(for: recorder.single, timeout: ...)
  6. XCTAssertEqual(element, "foo")
  7. }
  8. // SUCCESS: no error
  9. func testJustSynchronouslyPublishesExactlyOneElement() throws {
  10. let publisher = Just("foo")
  11. let recorder = publisher.record()
  12. let element = try recorder.single.get()
  13. XCTAssertEqual(element, "foo")
  14. }

Examples of failing tests

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