SkillAgentSearch skills...

CombineExpectations

Utilities for tests that wait for Combine publishers

Install / Use

/learn @groue/CombineExpectations
About this skill

Quality Score

0/100

Supported Platforms

Universal

README

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]
  • [Installation]
  • [Publisher Expectations]: [availableElements], [completion], [elements], [finished], [last], [next()], [next(count)], [prefix(maxLength)], [recording], [single]

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]: all published elements until timeout expiration
  • [completion]: the publisher completion
  • [elements]: all published elements until successful completion
  • [finished]: the publisher successful completion
  • [last]: the last published element
  • [next()]: the next published element
  • [next(count)]: the next N published elements
  • [prefix(maxLength)]: the first N published elements
  • [recording]: the full recording of publisher events
  • [single]: the one and only published element

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:

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

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

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

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:

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

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:

// 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()
}
<details> <summary>Examples of failing tests</summary>
// FAIL: Asynchronous wait failed
func testFinishedTimeout() throws {
    let publisher = PassthroughSubject<String, Never>()
    let recorder = publisher.record()
    try wait(for: recorder.finished, timeout: ...)
}

// FAIL: Caught err
View on GitHub
GitHub Stars257
CategoryDevelopment
Updated2mo ago
Forks17

Languages

Swift

Security Score

100/100

Audited on Jan 15, 2026

No findings