Testable Combine Publishers - An easy, declarative way to unit test Combine Publishers in Swift

Last update: Jun 16, 2022

Testable Combine Publishers

An easy, declarative way to unit test Combine Publishers in Swift

Example Combine Unit Test

About

Combine Publishers are notoriously verbose to unit test. They require you to write complex Combine chains in Swift for each test, keeping track of AnyCancellables, and interweaving XCTestExpectations, fulfillment requirements, and timeouts.

This Swift Package aims to simplify writing unit tests for Combine Publishers by providing a natural spelling of .expect(...) for chaining expectations on the Publisher subject. The resulting PublisherExpectation type collects the various expectations and then provides a way to assert that the expectations are fulfilled by calling .waitForExpectations(timeout: 1)

Under the hood, PublisherExpectation is utilizing standard XCTest framework APIs and forwarding those assertion results to the corresponding lines of code that declared the expectation. This allows you to quickly see which specific expectation, in a chain of expectations, is failing in your unit tests, both in Xcode and in the console output.

Usage

In an XCTestCase, add a new unit test function, as normal, preparing the Publisher test subject to be tested. Utilize any combination of the examples below to validate the behavior of any Publisher in your unit tests.

Examples

For a Publisher that is expected to emit a single value and complete with .finished

func testSingleValueCompletingPublisher() {
    somePublisher
        .expect(someEquatableValue)
        .expectSuccess()
        .waitForExpectations(timeout: 1)
}

For a Publisher that is expected to emit multiple values, but is expected to not complete

func testMultipleValuePersistentPublisher() {
    somePublisher
        .collect(someCount)
        .expect(someEquatableValueArray)
        .expectNoCompletion()
        .waitForExpectations(timeout: 1)
}

For a Publisher that is expected to fail

func testPublisherFailure() {
    somePublisher
        .expectFailure()
        .waitForExpectations(timeout: 1)
}

For a Publisher that is expected to emit a value after being acted upon externally

func testLoadablePublisher() {
    let test = someDataSource.publisher
        .expect(someEquatableValue)
    someDataSource.load()
    test.waitForExpectations(timeout: 1)
}

For a Publisher expected to emit a single value whose Output is not Equatable

func testNonEquatableSingleValue() {
    somePublisher
        .expect({ value in
            if case .loaded(let model) = value, !model.rows.isEmpty { } else {
                XCTFail("Expected loaded and populated model")
            }
        })
        .waitForExpectations(timeout: 1)
}

For a Publisher that should emit a specific non-Equatable Error

func testNonEquatableFailure() {
    somePublisher
        .expectFailure({ failure in 
            switch failure {
            case .noInternet, .airPlaneMode:
                break
            default:
                XCTFail("Expected connectivity error")
            }
        })
        .waitForExpectations(timeout: 1)
}

Available Expectations

Value Expectations

  • expect(_ expected: Output) - Asserts that the provided Equatable value will be emitted by the Publisher
  • expectNot(_ expected: Output) - Asserts that a value will be emitted by the Publisher and that it does NOT match the provided Equatable
  • expect(_ assertion: (Output) -> Void) - Invokes the provided assertion closure on every value emitted by the Publisher. Useful for calling XCTAssert variants where custom evaluation is required

Success Expectations

  • expectSuccess() - Asserts that the Publisher data stream completes with a success status (.finished)

Failure Expectations

  • expectFailure() - Asserts that the Publisher data stream completes with a failure status (.failure(Failure))
  • expectFailure(_ failure: Failure) - Asserts that the provided Equatable Failure type is returned when the Publisher completes
  • expectNotFailure(_ failure: Failure) - Asserts that the Publisher completes with a Failure type which does NOT match the provided Equatable Failure
  • expectFailure(_ assertion: (Failure) -> Void) - Invokes the provided assertion closure on the Failure result's associated Error value of the Publisher. Useful for calling XCTAssert variants where custom evaluation is required

Completion Expectations

  • expectCompletion() - Asserts that the Publisher data stream completes, indifferent of the returned success/failure status
  • expectNoCompletion() - Asserts that the Publisher data stream does NOT complete. ⚠️ This will wait for the full timeout in waitForExpectations(timeout:)
  • expectCompletion(_ assertion: (Completion) -> Void) - Invokes the provided assertion closure on the recieveCompletion handler of the Publisher. Useful for calling XCTAssert variants where custom evaluation is required

Upcoming Features

  • Support for working with Schedulers to avoid relying on timeouts

GitHub

https://github.com/albertbori/TestableCombinePublishers
You might also like...

🌤 Swift Combine extensions for asynchronous CloudKit record processing

Swift Combine extensions for asynchronous CloudKit record processing. Designed for simplicity.

May 22, 2022

AnalyticsKit for Swift is designed to combine various analytical services into one simple tool.

🦋 AnalyticsKit AnalyticsKit for Swift is designed to combine various analytical services into one simple tool. To send information about a custom eve

Jan 14, 2022

The fastest 🚀 way to embed a 3D model in Swift

The fastest 🚀 way to embed a 3D model in Swift

Insert3D is the easiest 🥳 and fastest 🚀 way to embed a 3D model in your iOS app. It combines SceneKit and Model I/O into a simple library for creati

Apr 23, 2022

Handy Combine extensions on NSObject, including SetAnyCancellable.

Handy Combine extensions on NSObject, including Set<AnyCancellable>.

Storable Description If you're using Combine, you've probably encountered the following code more than a few times. class Object: NSObject { var c

May 13, 2022

Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data.

Pigeon 🐦 Introduction Pigeon is a SwiftUI and UIKit library that relies on Combine to deal with asynchronous data. It is heavily inspired by React Qu

Jun 8, 2022

Use this package in order to ease up working with Combine URLSession.

Use this package in order to ease up working with Combine URLSession. We support working with Codable for all main HTTP methods GET, POST, PUT and DELETE. We also support MultipartUpload

Jan 6, 2022

Swift-DocC is a documentation compiler for Swift frameworks and packages aimed at making it easy to write and publish great developer documentation.

Swift-DocC is a documentation compiler for Swift frameworks and packages aimed at making it easy to write and publish great developer docum

Jun 22, 2022

Sovran-Swift: Small, efficient, easy. State Management for Swift

Sovran-Swift: Small, efficient, easy. State Management for Swift

May 17, 2022

The simplest way to display the librarie's licences used in your application.

The simplest way to display the librarie's licences used in your application.

Features • Usage • Translation • Customisation • Installation • License Display a screen with all licences used in your application can be painful to

Feb 28, 2022
Project shows how to unit test asynchronous API calls in Swift using Mocking without using any 3rd party software

UnitTestingNetworkCalls-Swift Project shows how to unit test asynchronous API ca

May 6, 2022
CombineDriver - Drivers are Publishers which don't complete

CombineDriver Drivers are Publishers which don't complete. CombineDriver is a sm

May 21, 2022
A declarative, thread safe, and reentrant way to define code that should only execute at most once over the lifetime of an object.

SwiftRunOnce SwiftRunOnce allows a developer to mark a block of logic as "one-time" code – code that will execute at most once over the lifetime of an

Apr 11, 2022
Unit conversion library for Swift.

MKUnits MKUnits is extremely precise unit conversion library for Swift. It provides units of measurement of physical quantities and simplifies manipul

May 5, 2022
Unit converter in Swift
Unit converter in Swift

Scale ❤️ Support my app ❤️ Push Hero - pure Swift native macOS application to test push notifications PastePal - Pasteboard, note and shortcut manager

Jun 18, 2022
Pure Declarative Programming in Swift, Among Other Things

Basis The Basis is an exploration of pure declarative programming and reasoning in Swift. It by no means contains idiomatic code, but is instead inten

Jun 20, 2022
Easy way to detect iOS device properties, OS versions and work with screen sizes. Powered by Swift.
Easy way to detect iOS device properties, OS versions and work with screen sizes. Powered by Swift.

Easy way to detect device environment: Device model and version Screen resolution Interface orientation iOS version Battery state Environment Helps to

Jun 4, 2022
Demonstration of Cocoapod test targets failing to build when integrated with TestingExtensions 0.2.11.

TestingExtensions0_2_11-Bug Symptoms Open project, hit test (Command+U), TestingExtensions fails to compile with a list of errors appearing to be rela

Dec 27, 2021
It is a simple maths quiz app that will help users to test their math skills.

MathQuiz It is a simple maths quiz app that will help users to test their math skills. It has the following screens 1.Welcome screen with start button

Dec 27, 2021
A simple Pokedex app written in Swift that implements the PokeAPI, using Combine and data driven UI.
A simple Pokedex app written in Swift that implements the PokeAPI, using Combine and data driven UI.

SwiftPokedex SwiftPokedex is a simple Pokedex app written by Viktor Gidlöf in Swift that implements the PokeAPI. For full documentation and implementa

May 7, 2022