Skip to content

groue/CombineExpectations

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

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:

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:

 import PackageDescription let package = Package( dependencies: [ + .package(url: "https://github.com/groue/CombineExpectations.git", ...) ], targets: [ .testTarget( dependencies: [ + "CombineExpectations" ]) ] )

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.

❌ 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.

➑️ 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:

// 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

πŸ• recorder.completion 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 is returned.

➑️ Related expectations: finished, recording.

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: ...) 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)") } }
Examples of failing tests
// 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

πŸ• 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.

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

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: ...) XCTAssertEqual(elements, ["foo", "bar", "baz"]) } // 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"]) }
Examples of failing tests
// 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

πŸ• recorder.finished waits for the recorded publisher to complete.

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

➑️ Related expectations: completion, recording.

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: ...) } // SUCCESS: no error func testArrayPublisherSynchronouslyFinishesWithoutError() throws { let publisher = ["foo", "bar", "baz"].publisher let recorder = publisher.record() try recorder.finished.get() }
Examples of failing tests
// 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:

// SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotFinish() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() try wait(for: recorder.finished.inverted, timeout: ...) }
Examples of failing tests
// 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

πŸ• 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.

➑️ Related expectations: elements, single.

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: ...) { 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.

➑️ Related expectations: next(count), single.

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: ...) XCTAssertEqual(element, "foo") element = try wait(for: recorder.next(), timeout: ...) XCTAssertEqual(element, "bar") } // 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") }
Examples of failing tests
// 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:

// SUCCESS: no timeout, no error func testPassthroughSubjectDoesNotPublishAnyElement() throws { let publisher = PassthroughSubject<String, Never>() let recorder = publisher.record() try wait(for: recorder.next().inverted, timeout: ...) }
Examples of failing tests
// 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)

πŸ• 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.

➑️ Related expectations: next(), prefix(maxLength).

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: ...) XCTAssertEqual(elements, ["foo", "bar"]) elements = try wait(for: recorder.next(1), timeout: ...) XCTAssertEqual(elements, ["baz"]) } // 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"]) }
Examples of failing tests
// 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)

πŸ• recorder.prefix(maxLength) 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.

➑️ Related expectations: availableElements, elements, next(count).

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: ...) XCTAssertEqual(elements, ["foo", "bar"]) } // 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"]) }
Examples of failing tests
// 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:

// 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: ...) XCTAssertEqual(elements, ["foo", "bar"]) }
Examples of failing tests
// 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

πŸ• recorder.recording 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 is returned.

➑️ Related expectations: completion, elements, finished.

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: ...) XCTAssertEqual(recording.output, ["foo", "bar", "baz"]) if case let .failure(error) = recording.completion { XCTFail("Unexpected error \(error)") } } // 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)") } }
Examples of failing tests
// 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

πŸ• 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.

➑️ Related expectations: elements, last, next().

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

About

Utilities for tests that wait for Combine publishers

Topics

Resources

License

Stars

Watchers

Forks

Sponsor this project

 

Packages

No packages published

Contributors 3

  •  
  •  
  •