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

Overview

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
You might also like...
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

🌤 Swift Combine extensions for asynchronous CloudKit record processing

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

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

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

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

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

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

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

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

Simple way to set up half modal view
Simple way to set up half modal view

HalfModalView Requirements iOS 9.0+ Xcode 10.0+ Swift 4.0+ Example Installation CocoaPods is a dependency manager for Cocoa projects. You can install

Comments
  • AutomaticallyEquatable

    AutomaticallyEquatable

    Added the AutomaticallyEquatable protocol. Custom types that conform to this protocol by way of extension will automatically conform to Equatable. This default equatable implementation uses reflection to recursively compare each member's values.

    Benefits

    The main purpose of this type is to further reduce the amount of code required to use the Publisher.expect(...) extension.

    As an additional benefit, it provides a custom unit test failure message concatenated to the XCTAssertEqual error message which tells you exactly what's different about the two compared values. For example, it might read this if one of the array elements in some sub-object is missing:

    Person.relationships.7: friend(Person(name: "Bar", relationships: [])) is not equal to nil

    Disclosure

    It has the following important limitations:

    • Cannot anticipate the consequences of observing a calculated property. (ie, code that changes the state of data when a property is observed).
    • Cannot respect custom Equatable implementations of the values being compared or any of the subsequent members. It will use its own comparison logic instead.
    • Skips over members that cannot be reasonably compared, such as closures. These are assumed to be equal.
    • Does not support recursive evaluation of reflexive types (it will crash if a property on a type references itself)

    Usage

    class Baz {
        let answer: Int
        init(answer: Int) {
            self.answer = answer
        }
    }
    
    enum MyCustomType {
        case foo
        case bar(Baz)
    }
    
    extension MyCustomType: AutomaticallyEquatable { /*no-op*/ }
    
    func testSomePublisher() {
        SomeSubject()
            .loadPublisher()
            .expect(MyCustomType.bar(Baz(answer: 42))
            .waitForExpectations(timeout: 1)
    }
    
    opened by albertbori 0
  • Async Publisher support for asserting first value

    Async Publisher support for asserting first value

    Wanted to leverage async await for the times where you only need a single value from a publisher. This API will improve testing ergonomics and will improve test stability because tests will always wait for an appropriate time instead of guessing how long a test needs to run with waitForExpectations.

    opened by PorterHoskins 0
Releases(1.1.0)
  • 1.1.0(Sep 6, 2022)

    What's Changed

    • AutomaticallyEquatable by @albertbori in https://github.com/albertbori/TestableCombinePublishers/pull/2

    New Contributors

    • @albertbori made their first contribution in https://github.com/albertbori/TestableCombinePublishers/pull/2

    Full Changelog: https://github.com/albertbori/TestableCombinePublishers/compare/1.0.4...1.1.0

    Source code(tar.gz)
    Source code(zip)
  • 1.0.4(Jun 15, 2022)

Owner
Albert Bori
Albert Bori
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

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

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

GOOD HATS LLC 0 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

Thumbtack 8 Aug 17, 2022
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

Khoa 324 Dec 10, 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

Michal Konturek 343 Sep 21, 2022
Unit converter in Swift

Unit converter in Swift

Khoa 324 Jun 29, 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

TypeLift 314 Dec 22, 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

Sihao Lu 0 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

null 0 Dec 27, 2021
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

Anatoliy Voropay 582 Dec 25, 2022