CombineExpectations 0.9.0

CombineExpectations 0.9.0

Gwendal RouéGwendal RouéGwendal Roué 维护。



  • 作者
  • Gwendal Roué

Combine Expectations

用于等待 Combine 发布者的测试工具。


最新发布版本: 版本 0.7.0 (2021 年 1 月 9 日) • 发行说明

要求: iOS 13.0+ / macOS 10.15+ / tvOS 13.0+ • Swift 5.1+ / Xcode 11.0+

联系方式: 在 GitHub 问题 中报告错误并提问。


使用 XCTestExpectation 测试 Combine 发布者通常需要一个大量的样板代码。

CombineExpectations 致力于简化这些测试。它定义了一个等待 发布者预期 的 XCTestCase 方法。


用法

等待 发布者预期 可以使您的测试看起来像这样

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!"])
    }
}

当您等待发布者预期时

  • 如果预期在指定的超时时间内未得到满足,则测试失败。
  • 如果无法返回预期的值,则会抛出错误。例如,等待 recorder.elements 如果发布者因错误而完成,则会抛出发布者错误。
  • 如果期望已经达到等待的状态,则 wait 方法会立即返回。

可以为发布者多次等待。

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

并非所有测试都需要等待,因为某些发布者的期望可以立即满足。在这种情况下,最好是使用同步的 get() 方法,而不是使用 wait(for:timeout:),如下所示:

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!"])
    }
}

wait(for:timeout:) 类似,get() 方法可以多次调用。

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")
    }
}

安装

在你的 Swift 包管理器 测试目标中添加对 CombineExpectations 的依赖。

 import PackageDescription
 
 let package = Package(
     dependencies: [
+        .package(url: "https://github.com/groue/CombineExpectations.git", ...)
     ],
     targets: [
         .testTarget(
             dependencies: [
+                "CombineExpectations"
             ])
     ]
 )

发布者期望

有各种发布者期望。每一个都等待特定的发布者方面。


availableElements

🕝 recorder.availableElements 等待期望超时或已记录的发布者完成。

在等待此期望时,如果发布者在期望超时之前失败,抛出发布者错误。

否则,返回超时到期之前发布的所有元素数组。

➡️相关期望:`elements`、`prefix(maxLength)`。

与其它期望不同,`availableElements` 在超时到期时不会导致测试失败。它只返回到目前为止发布的元素。

示例

// 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 等待已记录的发布者完成。

等待此期望时,如果发布者未按时完成,则抛出 RecordingError.notCompleted

否则,返回 Subscribers.Completion

➡️相关期望:finishedrecording

示例

// 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)")
    }
}
失败测试的示例
// 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 等待已记录的发布者完成。

等待此期望时,如果发布者未按时完成,则抛出 RecordingError.notCompleted,并在发布者失败时抛出发布者错误。

否则,返回发布元素的数组。

➡️相关期望:availableElementslastprefix(maxLength)recordingsingle

示例

// 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"])
}
失败测试的示例
// 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 等待已记录的发布者完成。

等待此期望时,如果发布者失败,则抛出发布者错误。

➡️相关期望:completionrecording

示例

// 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()
}
失败测试的示例
// 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 可逆

// SUCCESS: no timeout, no error
func testPassthroughSubjectDoesNotFinish() throws {
    let publisher = PassthroughSubject<String, Never>()
    let recorder = publisher.record()
    try wait(for: recorder.finished.inverted, timeout: ...)
}
失败测试的示例
// 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 等待已记录的发布者完成。

等待此期望时,如果发布者未按时完成,则抛出 RecordingError.notCompleted,并在发布者失败时抛出发布者错误。

否则,返回最后一个发布的元素,或者在发布者完成前未发布任何元素时返回 nil。

➡️相关期望:elementssingle

示例

// 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")
    }
}
失败测试的示例
// 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() 等待已记录的发布者发出一个元素,或者完成。

在此期望等待时,如果发布者在此等待的期望之后没有发布一个元素,则抛出 RecordingError.notEnoughElements。如果发布者在发布下一个元素之前失败,则会抛出发布者错误。

否则,将返回下一个发布的元素。

➡️相关期望: next(count)single

示例

// 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")
}
失败测试的示例
// 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() 可以反转。

// SUCCESS: no timeout, no error
func testPassthroughSubjectDoesNotPublishAnyElement() throws {
    let publisher = PassthroughSubject<String, Never>()
    let recorder = publisher.record()
    try wait(for: recorder.next().inverted, timeout: ...)
}
失败测试的示例
// 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) 会等待记录的发布者发布 count 个元素或完成。

在此期望等待时,如果发布者在最后等待的期望之后没有发布 count 个元素,则抛出 RecordingError.notEnoughElements。如果发布者在发布下一个 count 个元素之前失败,则会抛出发布者错误。

否则,将返回包含正好 count 个元素的数组。

➡️相关期望:next()prefix(maxLength)

示例

// 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"])
}
失败测试的示例
// 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) 会等待记录的发布者发布 maxLength 个元素或完成。

在等待此期望时,如果发布者在发布 maxLength 个元素之前失败,则抛出发布者错误。

否则,将返回一个元素数组,最多包含 maxLength 个元素,或如果发布者提前完成,则少于 maxLength 个元素。

➡️相关期望:availableElementselementsnext(count)

示例

// 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"])
}
失败测试的示例
// 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) 可以反转。

// 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"])
}
失败测试的示例
// 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 会等待记录的发布者完成。

等待此期望时,如果发布者未按时完成,则抛出 RecordingError.notCompleted

否则,将返回一个 Record.Recording

➡️相关期望:completionelementsfinished

示例

// 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)")
    }
}
失败测试的示例
// 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 等待被记录的发布者完成。

当等待这个期望时,如果发布者未能及时完成,或者完成前没有恰好发布一个元素,则会抛出 RecordingError 异常。如果发布者失败,则会抛出发布者错误。

否则,将返回已发布的单个元素。

➡️相关期望:元素最后next()

示例

// 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")
}
失败测试的示例
// 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: ...)
}