ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Swift Composable Architecture 测试指南:深入解析 TestStore 的穷尽性(Exhaustivity)配置

Swift Composable Architecture 测试指南:深入解析 TestStore 的穷尽性(Exhaustivity)配置 前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载本文以 Swift Composable ArchitectureTCA的TestStore穷尽性exhaustivity机制为主线系统讲解穷尽测试与非穷尽测试的适用场景、配置方式与底层实现。读完本文你将掌握如何通过store.exhaustivity .off与非穷尽断言快速编写大型功能集成测试同时仍保留对关键状态的精确校验能力并理解库内部为何在非穷尽模式下放宽断言要求。什么是 TestStore 的穷尽性TCA 的TestStore默认采用穷尽测试exhaustive testing模式测试必须对每一次状态变化、每一个由 Effect 回传的 Action 进行断言并且所有在途in-flightEffect 都必须在测试结束前被接收处理完毕。正如 TestStore.swift 文档注释所述But, if in the future a bug is introduced causing a search request to be executed even when the query is empty, you will get a test failure because a new effect is being created that is not being asserted on. This is the power of exhaustive testing.穷尽测试的威力在于任何未被断言的副作用都会直接导致测试失败从而把预期之外的代码行为第一时间暴露出来。例如搜索功能中只要未来某次改动让空查询也触发了网络请求穷尽测试会立刻报错因为该 Effect 没有被断言。TestStore的穷尽性由公开属性exhaustivity控制默认值为.on见 TestStore.swiftpublic var exhaustivity: Exhaustivity .onExhaustivity 枚举的三种取值穷尽性配置的完整定义位于 TestStore.swift/// The exhaustivity of assertions made by the test store. public enum Exhaustivity: Equatable, Sendable { /// Exhaustive assertions. case on /// Non-exhaustive assertions. case off(showSkippedAssertions: Bool) /// Non-exhaustive assertions. public static let off Self.off(showSkippedAssertions: false) }三个取值对应的语义如下取值语义说明.on穷尽断言必须穷尽断言所有状态变化与 Effect 回传的 Action测试结束前所有在途 Effect 必须被接收。需要手动跳过时使用skipReceivedActions(strict:)与skipInFlightEffects(strict:)需要部分匹配 Action 时使用receive(_:timeout:assert:)的变体。.off非穷尽断言等价于.off(showSkippedAssertions: false)允许只断言任意子集的状态变化与 Action未断言的变更静默通过不产生任何提示。.off(showSkippedAssertions: true)非穷尽断言 显示被跳过的断言所有未断言的变更会以灰色信息框形式展示在对应断言旁帮助你了解正在忽略哪些信息但不导致测试失败。该枚举遵循Equatable与Sendable可以安全地在测试中比较或跨并发上下文传递。关闭穷尽性后的三大行为变化根据 TestStore.swift将exhaustivity设为.off后TestStore的行为会发生三点关键变化断言可以只覆盖部分状态变化send与receive的尾随闭包不再要求覆盖全部状态变化可以只断言任意子集只有当闭包内做出错误的修改时才会报告测试失败。允许在存在未断言 Action 时继续发送/接收即使 Effect 已经回传了尚未断言的 Action也可以继续调用send/receive未断言的待处理 Action 会被自动清除。允许带着未断言 Action 与在途 Effect 结束测试测试结束时即使存在未断言的已接收 Action 和尚未完成的在途 Effect也不会报告任何失败。这种宽松测试风格最适用于多特性集成测试当你想聚焦于某一行为切片时不必关心其他特性内部如何变化。配置方式一直接设置 exhaustivity 属性最直接的方式是在创建TestStore后立即设置exhaustivity属性例如文档中针对登录功能集成测试给出的示例TestStore.swiftlet store TestStore(App.State()) { App() } store.exhaustivity .off // ⬅️ 关闭穷尽性 await store.send(\.login.submitButtonTapped) await store.receive(\.login.delegate.didLogin) { $0.selectedTab .activity }这段测试只关心点击提交按钮后最终选中 Tab 切换到 activity完全不理会登录特性内部的状态变化与 Effect 数据流。配置方式二withExhaustivity 临时作用域当你不希望整个测试都处于非穷尽模式而只想在某个操作片段内临时切换时可以使用withExhaustivity(_:operation:)。TestStore 提供了同步与异步两个重载TestStore.swift/// 同步版本 public func withExhaustivityR( _ exhaustivity: Exhaustivity, operation: () throws - R ) rethrows - R { let previous self.exhaustivity defer { self.exhaustivity previous } self.exhaustivity exhaustivity return try operation() } /// 异步版本 public func withExhaustivityR( _ exhaustivity: Exhaustivity, operation: () async throws - sending R ) async rethrows - R { let previous self.exhaustivity defer { self.exhaustivity previous } self.exhaustivity exhaustivity return try await operation() }两个版本都遵循保存原值 → 设置新值 → 执行操作 → 恢复原值的defer模式因此作用域结束后穷尽性自动还原非常适合在测试中部临时放宽约束。有趣的是库内部在非穷尽模式产生断言差异时也会通过self.withExhaustivity(.on) { ... }临时回到穷尽模式来生成精确的 diff 报告见 TestStore.swift。完整实战穷尽 vs 非穷尽编写登录集成测试文档用3 个 Tab 的应用其中第 3 个 Tab 是登录页作为典型案例TestStore.swift。用户点击登录后登录特性内部会发生一连串事件发起 API 请求、接收响应、发送 delegate Action 通知父级最终第三个 Tab 从登录页切换到资料页同时选中 Tab 切换到第一个活动Tab。穷尽风格的写法穷尽风格要求你完整模拟登录特性的每一次状态变化和 Effect 回传let store TestStore(initialState: App.State()) { App() } // 1️⃣ 模拟用户点击提交按钮 // 可以使用 case key path 语法将 Action 发送到深层嵌套的特性 await store.send(\.login.submitButtonTapped) { // 2️⃣ 断言登录特性中的所有状态变化 $0.login?.isLoading true … } // 3️⃣ 登录特性执行 API 请求并将响应回传到系统中 await store.receive(\.login.loginResponse.success) { // 4️⃣ 断言登录特性中的所有状态变化 $0.login?.isLoading false … } // 5️⃣ 登录特性发送 delegate Action通知父级特性已成功登录 await store.receive(\.login.delegate.didLogin) { // 6️⃣ 断言因该 Action 引起的所有 App 状态变化 $0.authenticatedTab .loggedIn( Profile.State(...) ) … // 7️⃣ *最终*断言选中 Tab 切换到 activity $0.selectedTab .activity }文档明确指出这种写法存在三个痛点TestStore.swift必须深入了解登录特性内部实现才能逐个断言其状态变化与 Effect 数据流登录特性逻辑一旦调整即使本测试关心的行为并未变化也可能连锁失败测试冗长遇到类似但略有差异的流程时容易复制粘贴产生大量脆弱、重复的测试代码。非穷尽风格的写法非穷尽风格只关心登录导致选中 Tab 切换这个高层流程let store TestStore(App.State()) { App() } store.exhaustivity .off // ⬅️ await store.send(\.login.submitButtonTapped) await store.receive(\.login.delegate.didLogin) { $0.selectedTab .activity }我们完全没有断言登录特性的状态变化和 Effect 数据流只断言点击 Submit 后最终收到didLogindelegate Action并把选中 Tab 切换到 activity。登录特性此后可以自由调整内部逻辑完全不影响这条集成测试。showSkippedAssertions查看被忽略的变更store.exhaustivity .off会让所有未断言的变更静默通过。如果你希望了解测试正在忽略什么、生产环境可能的 bug 藏在哪可以改用.off(showSkippedAssertions: true)TestStore.swiftlet store TestStore(initialState: App.State()) { App() } store.exhaustivity .off(showSkippedAssertions: true) // ⬅️ await store.send(\.login.submitButtonTapped) await store.receive(\.login.delegate.didLogin) { $0.selectedTab .profile }运行后每条未完整断言的断言旁会出现灰色信息框展示被跳过的变更文档示例输出TestStore.swift◽️ Expected failure: A state change does not match expectation: …App.State( authenticatedTab: .loggedOut( Login.State( - isLoading: false isLoading: true, … ) ) )Skipped receiving .login(.loginResponse(.success))A state change does not match expectation: …App.State( - authenticatedTab: .loggedOut(…) authenticatedTab: .loggedIn( Profile.State(…) ), … )(Expected: −, Actual: )这些提示不会导致测试失败只是告知你哪些变更未被显式断言对排查生产中发生但测试未捕获的 bug 很有帮助。源码级解析非穷尽模式在内部如何工作send 方法中的分支处理TestStore.send在发送 Action 前会根据当前exhaustivity处理积压的已接收 ActionTestStore.swiftswitch self.exhaustivity { case .on: break case .off(showSkippedAssertions: true): await self.skipReceivedActions(strict: false) case .off(showSkippedAssertions: false): self.reducer.receivedActions [] }可以看到穷尽模式.on下若存在未处理的已接收 Actionsend会先报告 Must handle N received actions before sending an action 错误TestStore.swift非穷尽模式则直接清理积压队列——showSkippedAssertions: true时通过skipReceivedActions(strict: false)以灰色提示方式记录被跳过的 ActionshowSkippedAssertions: false时直接清空。expectedStateShouldMatch 中的断言对比逻辑断言对比核心方法expectedStateShouldMatch在非穷尽模式下将期望状态的基准从发送前的状态改为实际状态TestStore.swiftcase .off: var expectedWhenGivenActualState actual if let updateStateToExpectedResult { try Dependencies.withDependencies { $0 self.reducer.dependencies } operation: { try self.sharedChangeTracker.assert { try updateStateToExpectedResult(expectedWhenGivenActualState) } } } expected expectedWhenGivenActualState if expectedWhenGivenActualState ! actual { self.withExhaustivity(.on) { expectationFailure(expected: expectedWhenGivenActualState) } } else if self.exhaustivity .off(showSkippedAssertions: true) …这段实现揭示了两条关键规则只断言子集也能通过断言闭包基于actual当前真实状态进行修改只要闭包做出的修改与真实状态一致即便还有大量其他状态变化未被覆盖测试也通过。错误断言仍然失败如果闭包修改后的状态与真实状态不一致比如把count改成错误值会临时切换回.on并生成带 diff 的失败报告保证宽松但不放过错误。另外闭包在执行时通过XCTModifyLocals.$isExhaustive注入当前穷尽性供状态包装类型如PresentationState判断是否允许部分修改TestStore.swift。测试用例佐证TestStoreNonExhaustiveTests仓库的 TestStoreNonExhaustiveTests.swift 用大量测试验证了上述行为例如部分断言合法testNonExhaustiveSend_PartialExhaustive在store.exhaustivity .off(showSkippedAssertions: true)下三次send(.increment)分别只断言count、isEven等字段的一部分注释明确写着// Ignoring state change: ...测试照常通过TestStoreNonExhaustiveTests.swift错误断言仍失败testNonExhaustiveSend_PartialExhaustive_BadAssertion使用XCTExpectFailure断言当闭包做出错误修改时仍会产出 A state change does not match expectation 的 diff 失败报告TestStoreNonExhaustiveTests.swift跳过已接收 Action 与在途 EffecttestSkipReceivedActions_PartialExhaustive演示了.off(showSkippedAssertions: true)下skipReceivedActions(strict: false)的用法testCancelInFlightEffects_NonStrict/Strict则验证了skipInFlightEffects(strict:)在无在途 Effect 时的严格失败行为TestStoreNonExhaustiveTests.swift。非穷尽模式下常用的配套 API非穷尽模式常与以下 TestStore API 搭配使用finish(timeout:)等待所有在途 Effect 执行完毕让测试可以跑完整个副作用流程后再断言最终状态skipReceivedActions(strict:)跳过所有尚未断言的已接收 Actionstrict: true时若没有可跳过的 Action 会报告失败skipInFlightEffects(strict:)取消并跳过所有在途 Effectassert(_:)直接断言 store 的当前状态是发送 → 跑完 → 跳过 → 断言最终态这一非穷尽工作流的收尾步骤TestStore.swiftstore.exhaustivity .off await store.send(\.child.closeButtonTapped) await store.finish() await store.skipReceivedActions() store.assert { $0.child nil }官方文档特别说明assert只适用于非穷尽测试商店穷尽模式下无需使用因为所有断言已由之前的send/receive完成。何时使用穷尽、何时使用非穷尽综合文档建议与源码注释可以总结出以下选择原则场景推荐模式原因叶子特性leaf node features的单元测试.on默认希望穷尽断言特性内部发生的一切精确锁定所有状态变化与 Effect 行为多特性集成测试.off或.off(showSkippedAssertions: true)聚焦某一段行为切片不关心其他特性的内部实现细节排查生产中发生但测试未捕获的 bug.off(showSkippedAssertions: true)灰色提示框会列出所有被跳过的断言帮助定位遗漏的校验点值得一提的是非穷尽测试商店这一概念最早由 Krzysztof Zabłocki 在博客文章与会议演讲中提出后来被整合进 TCA 核心库见 TestStore.swift 的注释引用。结语TestStore.exhaustivity是 TCA 测试体系中精确性与灵活性之间的调节旋钮默认的.on为你提供最严格的穷尽校验防止任何未被断言的副作用悄悄溜走.off则让你在大型集成测试中聚焦高层行为showSkippedAssertions参数更是在两者之间提供保留可见性、不阻塞测试的中间态。结合 TestingTCA.md 与 TestStore.md 的完整 API 文档以及 TestStoreNonExhaustiveTests.swift 的测试用例你可以为每个特性精准选择合适的测试策略。赞分享前端移动开发【免费下载链接】swift-composable-architectureA library for building applications in a consistent and understandable way, with composition, testing, and ergonomics in mind.项目地址https://gitcode.com/GitHub_Trending/sw/swift-composable-architecture点击查看免费下载相关推荐docz-core 演进全解读从 CLI 命令编排到 Gatsby 驱动的文档构建内核v0.1 → v2.4docz core 演进全解读从 CLI 命令编排到 Gatsby 驱动的文档构建内核v0.1 → v2.4 docz core 是 docz 项目一个前端移动开发TestStore 弃用 API 迁移指南swift-composable-architecture 中已废弃测试接口的识别与替换TestStore 弃用 API 迁移指南swift composable architecture 中已废弃测试接口的识别与替换 在 swift compo前端移动开发Swift Composable Architecture 状态共享完全指南从 Shared 到持久化策略与穷举测试Swift Composable Architecture 状态共享完全指南从 Shared 到持久化策略与穷举测试 本指南围绕 swift composa前端移动开发上一篇终极Dio并发请求优化指南如何控制最大并发数提升性能下一篇VoltAgent 遥测导出实战基于 with-voltagent-exporter 示例接入 VoltOps 可观测性平台创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
返回列表