Skip to content
A set of tools making writing tests for Apple's Combine framework easy.
Swift
Branch: master
Clone or download
Fetching latest commit…
Cannot retrieve the latest commit at this time.
Permalink
Type Name Latest commit message Commit time
Failed to load latest commit information.
.github/workflows
Sources/CombineTestExtensions
Tests
.gitignore
.swiftformat
LICENSE
Package.swift
README.md

README.md

Combine Test Extensions

Build Status SPM Compatible Platforms: macOS+iOS+tvOS Twitter


A set of tools making writing tests for Apple's Combine framework easy. Inspired by RxTest and RxBlocking.


Recorder

A Subscriber implementation which records all values from the attached publisher.

import CombineTestExtensions

let publisher = PassthroughSubject<Int, TestError>()
let recorder = publisher.record()

let queue = DispatchQueue.global(qos: .default)
queue.asyncAfter(deadline: .now() + 0.1) { publisher.send(1) }
queue.asyncAfter(deadline: .now() + 0.2) { publisher.send(2) }
queue.asyncAfter(deadline: .now() + 0.3) { publisher.send(completion: .finished) }

let records = recorder.waitAndCollectRecords()

XCTAssertEqual(records, [.value(1), .value(2), .completion(.finished)])

You can also specify the number of values you want to record. This is handy when the recorded publisher never completes.

import CombineTestExtensions

let publisher = PassthroughSubject<Int, Never>()
let recorder = publisher.record(numberOfRecords: 3)

let queue = DispatchQueue.global(qos: .default)
queue.asyncAfter(deadline: .now() + 0.1) { publisher.send(1) }
queue.asyncAfter(deadline: .now() + 0.2) { publisher.send(2) }
queue.asyncAfter(deadline: .now() + 0.3) { publisher.send(3) }
queue.asyncAfter(deadline: .now() + 0.4) { publisher.send(4) }

let records = recorder.waitAndCollectRecords()

XCTAssertEqual(records, [.value(1), .value(2), .value(3)])

TimedRecorder

TimedRecorder is a subclass of Recorder expanding its functionality by adding timestamps to recorded events. It's not that useful by itself but it really shines in combination with TestPublisher. To create a new TimerRecorder instance, you have to provide it with a TestScheduler:

let scheduler = TestScheduler()
let timedRecorder = some_Publisher.record(scheduler: scheduler)

TestScheduler

A Scheduler implementation for using in tests. It uses Int for its time type. The scheduler is inactive until resume() is called. It doesn't make much sense to use it alone. It's main purpose is to be used with TestPublisher and TimedRecorder.

This example explains how TestScheduler works:

import CombineTestExtensions

let scheduler = TestScheduler()

var result: [Int] = []

scheduler.schedule(after: 100) { result.append(3) }
scheduler.schedule(after: 50) { result.append(2) }
scheduler.schedule { result.append(1) }

scheduler.resume()

XCTAssertEqual(result, [1, 2, 3])

TestPublisher

TestPublisher allows you to schedule events to be sent at specific "virtual time" timestamps. It's the perfect match for TimedRecorder:

import CombineTestExtensions

let scheduler = TestScheduler()

let publisher: TestPublisher<Int, Never> = .init(scheduler, [
    (40, .completion(.finished)),
    (30, .value(300)),
    (20, .value(200)),
    (10, .value(100)),
])

let toString = publisher.map(String.init)

let records = toString
    .record(scheduler: scheduler)
    .waitAndCollectTimedRecords()

XCTAssertEqual(records, [
    (10, .value("100")),
    (20, .value("200")),
    (30, .value("300")),
    (40, .completion(.finished)),
])

Alternatives

If you need more advanced test tools for Combine, check out EntwineTest. It's almost a 1:1 re-implementation of RxTest with support for Combine-specific features like backpressure.

License and Credits

CombineTestExtensions is released under the MIT license. See LICENSE for details.

Created by Vojta Stavik @ Industrial Binaries.

You can’t perform that action at this time.