• Stars
    star
    251
  • Rank 161,862 (Top 4 %)
  • Language
    Swift
  • License
    MIT License
  • Created about 5 years ago
  • Updated over 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Utilities for tests that wait for Combine publishers

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

More Repositories

1

GRDB.swift

A toolkit for SQLite databases, with a focus on application development
Swift
6,844
star
2

GRMustache

Flexible and production-ready Mustache templates for MacOS Cocoa and iOS
Objective-C
1,443
star
3

GRMustache.swift

Flexible Mustache templates for Swift
Swift
598
star
4

Semaphore

A Synchronization Primitive for Swift Concurrency
Swift
511
star
5

GRDBCombine

GRDB ❀️ Combine
223
star
6

GRDBQuery

The SwiftUI companion for GRDB
Swift
216
star
7

SortedDifference

A general sequence diffing algorithm, with a very specific purpose
Swift
56
star
8

GRDBSnapshotTesting

The snapshot testing library for GRDB
Swift
32
star
9

CombineTraits

Combine Publishers with Guarantees
Swift
25
star
10

GRDBObjc

FMDB-compatible bindings to GRDB.swift
Objective-C
20
star
11

WWDCCompanion

A demo app for full text search with GRDB.swift
Swift
18
star
12

GRDBCollections

Support for large result sets with GRDB
Swift
9
star
13

GRDBDemo

An iOS app that demonstrates the SQLite library GRDB and its FetchedRecordsController
9
star
14

GRValidation

Validation toolkit for Swift 2
Swift
8
star
15

GRDBDiff

Diff algorithms for SQLite, based on GRDB
Swift
5
star
16

CocoaHeadsDemo

Swift
4
star
17

GRMustacheSPM

A Swift package that uses GRMustache.swift
Swift
2
star
18

CSQLite

C
1
star
19

ARTemplating

Very simplistic performance comparison of mustache.js in a UIWebView and GRMustache
Objective-C
1
star
20

GRDBIssue636

Reproducible crash reported in https://github.com/groue/GRDB.swift/issues/636
Swift
1
star
21

SE-0235

A playground which explores SE-0235: https://forums.swift.org/t/se-0235-add-result-to-the-standard-library/17752
Swift
1
star
22

GRMustacheBenchmark

GRMustache Benchmarks
Objective-C
1
star