Swift Combine 使用 PassthroughSubject 测试订阅者 从入门到精通二十二

本文详细介绍了SwiftCombine的各个方面,从基础概念到高级用法,如测试中的PassthroughSubject应用,如何控制发布者和订阅者行为,以及处理网络请求和错误。通过一系列实例演示了如何使用SwiftCombine进行开发并进行单元测试。
摘要由CSDN通过智能技术生成

Combine 系列

  1. Swift Combine 从入门到精通一
  2. Swift Combine 发布者订阅者操作者 从入门到精通二
  3. Swift Combine 管道 从入门到精通三
  4. Swift Combine 发布者publisher的生命周期 从入门到精通四
  5. Swift Combine 操作符operations和Subjects发布者的生命周期 从入门到精通五
  6. Swift Combine 订阅者Subscriber的生命周期 从入门到精通六
  7. Swift 使用 Combine 进行开发 从入门到精通七
  8. Swift 使用 Combine 管道和线程进行开发 从入门到精通八
  9. Swift Combine 使用 sink, assign 创建一个订阅者 从入门到精通九
  10. Swift Combine 使用 dataTaskPublisher 发起网络请求 从入门到精通十
  11. Swift Combine 用 Future 来封装异步请求 从入门到精通十一
  12. Swift Combine 有序的异步操作 从入门到精通十二
  13. Swift Combine 使用 flatMap 和 catch错误处理 从入门到精通十三
  14. Swift Combine 网络受限时从备用 URL 请求数据 从入门到精通十四
  15. Swift Combine 通过用户输入更新声明式 UI 从入门到精通十五
  16. Swift Combine 级联多个 UI 更新,包括网络请求 从入门到精通十六
  17. Swift Combine 合并多个管道以更新 UI 元素 从入门到精通十七
  18. Swift Combine 通过包装基于 delegate 的 API 创建重复发布者 从入门到精通十八
  19. Swift Combine 响应NotificationCenter 的更新 从入门到精通十九
  20. Swift Combine 使用 ObservableObject 与 SwiftUI 模型作为发布源 从入门到精通二十
  21. Swift Combine 使用 XCTestExpectation 测试发布者 从入门到精通二十一
    在这里插入图片描述

1. 使用 PassthroughSubject 测试订阅者

目的:为了测试订阅者或包含订阅者的代码,我们可以使用 PassthroughSubject 模拟发布源,明确地控制哪些数据被发送和何时发送。

当你单独测试订阅者时,你可以通过使用 passthroughSubject 模拟发布者以及使用相关的 .send() 方法触发更新来更精细的控制测试。

此模式依赖于订阅者在构建时设置发布者-订阅者生命周期的初始部分,并让代码保持等待直到提供数据。 使用 PassthroughSubject,发送数据以触发管道和订阅者闭包,或跟踪可以被验证的状态更改,即可控制测试代码本身。

当你测试订阅者对失败的反应时,这种测试模式也非常有效,否则可能会终止订阅。

使用这种测试构建方法的一般模式是:

  1. 设置你的 subscriber 和任何你想包含在测试中影响它的管道。
  2. 在测试中创建一个 PassthroughSubject,构造合适的输出类型和失败类型以与订阅者匹配。
  3. 为任何初始值或先决条件设置断言。
  4. 通过 subject 发送数据。
  5. 测试发送数据的结果 —— 直接测试数据或断言预期的状态更改。
  6. 如果需要,发送其他数据。
  7. 测试状态或其他变化的进一步演变。

此模式的示例如下:

UsingCombineTests/SinkSubscriberTests.swift - testSinkReceiveDataThenError

func testSinkReceiveDataThenError() {

    // setup - preconditions  1
    let expectedValues = ["firstStringValue", "secondStringValue"]
    enum TestFailureCondition: Error {
        case anErrorExample
    }
    var countValuesReceived = 0
    var countCompletionsReceived = 0

    // setup
    let simplePublisher = PassthroughSubject<String, Error>()  // 2

    let cancellable = simplePublisher  // 3
        .sink(receiveCompletion: { completion in
            countCompletionsReceived += 1
            switch completion {  // 4
            case .finished:
                print(".sink() received the completion:", String(describing: completion))
                // no associated data, but you can react to knowing the
                // request has been completed
                XCTFail("We should never receive the completion, the error should happen first")
                break
            case .failure(let anError):
                // do what you want with the error details, presenting,
                // logging, or hiding as appropriate
                print("received the error: ", anError)
                XCTAssertEqual(anError.localizedDescription,
                               TestFailureCondition.anErrorExample.localizedDescription)  // 5
                break
            }
        }, receiveValue: { someValue in  // 6
            // do what you want with the resulting value passed down
            // be aware that depending on the data type being returned,
            // you may get this closure invoked multiple times.
            XCTAssertNotNil(someValue)
            XCTAssertTrue(expectedValues.contains(someValue))
            countValuesReceived += 1
            print(".sink() received \(someValue)")
        })

    // validate
    XCTAssertEqual(countValuesReceived, 0)  // 7
    XCTAssertEqual(countCompletionsReceived, 0)

    simplePublisher.send("firstStringValue")  // 8
    XCTAssertEqual(countValuesReceived, 1)
    XCTAssertEqual(countCompletionsReceived, 0)

    simplePublisher.send("secondStringValue")
    XCTAssertEqual(countValuesReceived, 2)
    XCTAssertEqual(countCompletionsReceived, 0)

    simplePublisher.send(completion: Subscribers.Completion.failure(TestFailureCondition.anErrorExample))   // 9
    XCTAssertEqual(countValuesReceived, 2)
    XCTAssertEqual(countCompletionsReceived, 1)

    // this data will never be seen by anything in the pipeline above because
    // we have already sent a completion
    simplePublisher.send(completion: Subscribers.Completion.finished)  // 10
    XCTAssertEqual(countValuesReceived, 2)
    XCTAssertEqual(countCompletionsReceived, 1)
}
  1. 此测试设置了一些变量,以便在测试执行期间捕获和修改它们,用于验证 sink 代码的执行时间和工作方式。 此外,我们在此处定义了一个错误,以便在我们的测试代码中使用它来验证失败的情况。
  2. 此代码设置为使用 passthroughSubject 来驱动测试,但我们感兴趣的测试代码是订阅者。
  3. 该订阅者被配置在测试下(在这儿是一个标准的 sink)。 我们配置了在接收到数据和 completion 时会触发的代码。
  4. 在接收到 completion 时,我们对其调用 switch,添加了一个断言,如果 finish 被调用了,将不通过测试,因为我们期望只会生成 .failure completion
  5. Swift 中的测试错误是否相等没那么容易,但如果错误是你正在控制的代码,有时你可以使用 localizedDescription 作为测试收到的错误类型的便捷方式。
  6. receiveValue 闭包在考虑如何对收到的值进行断言时更为复杂。 由于我们在此测试过程中会收到多个值,我们有一些额外的逻辑来检查值是否在我们发送的集合内。 与 completion 的处理逻辑一样,我们还是增加测试特定变量,我们将在以后断言这些变量以验证状态和操作顺序。
  7. 在我们发送任何数据以仔细检查我们的假设之前,我们先验证计数变量。
  8. 在测试中,send() 触发了操作,之后我们就可以立即通过验证我们更新的测试变量来验证所产生的效果了。 在你自己的代码中,你可能无法(或不想要)修改你的订阅者,但你可能能够向对象提供私有/可测试的属性或途径,以类似的方式验证它们。
  9. 我们还使用 send() 发送一个 completion,在这个例子中是一个失败的 completion
  10. 最后的 send() 验证刚刚发生的失败事件 —— 当前发送的 finished completion 应该没有被处理,并且应该没有后续的状态更新再发生。

参考

https://heckj.github.io/swiftui-notes/index_zh-CN.html

代码

https://github.com/heckj/swiftui-notes

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值