ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

Ruddarr iOS 应用测试与 CI 实践:Mock 依赖、预览 Fixture 与自动化测试完整清单

Ruddarr iOS 应用测试与 CI 实践:Mock 依赖、预览 Fixture 与自动化测试完整清单

【免费下载链接】app

A native iOS companion app for Radarr and Sonarr instances.

项目地址:https://gitcode.com/gh_mirrors/app154/app
点击查看免费下载

Ruddarr 是一款开源的 iOS 原生 companion app(伴侣应用),用于在手机上管理 Radarr 和 Sonarr 影视媒体库实例。要让它稳定可靠,项目搭建了一套完整的测试与 CI(持续集成)体系:通过可切换的 Mock 依赖隔离真实服务器、用 Preview Content 目录下的 JSON Fixture 还原真实接口数据,再配合 20 个自动化测试文件与三阶段 CI 脚本,保证每次构建都经过验证。本文带你完整走读这套实践。

为什么需要 Mock:离线测试 Radarr/Sonarr 应用

测试一个依赖私有 API 的 iOS 伴侣应用,最大的难题是:本地开发机不一定连得上你的 Radarr/Sonarr 实例。Ruddarr 的解法是把所有网络调用抽象成一组闭包,再提供.live(真实请求)和.mock(模拟数据)两套实现,一行代码即可切换。

API 抽象层:一切皆闭包

核心在 API.swift:整个网络层被拆成RadarrAPI、SonarrAPI、InstanceAPI三个结构体,每个端点都是可替换的@Sendable闭包,例如:

var fetch: @Sendable (Instance) async throws -> [Movie]
  • 真实实现:API+Live.swift 调用/api/v3/movie等接口
  • 模拟实现:API+Mock.swift 返回本地 Fixture 数据
  • 依赖注入中心:Dependencies.swift 中的全局let dependencies: Dependencies = .live

本地一键切换 Mock 模式

在 Ruddarr.swift 的init()中,DEBUG 构建下留有两行被注释的代码:

// dependencies.api = .mock // dependencies.cloudkit = .mock

取消注释注释即可让 App 完全脱离网络和 iCloud 运行——CloudKit 同步同样有 mock 分支(见 InstanceWebhook.swift)。

Preview Content 预览 Fixture:26 个 JSON 还原真实数据

Mock 实现并不硬编码假数据,而是通过loadPreviewData函数(API+Mock.swift)从 Ruddarr/Preview Content/ 目录读取 JSON 文件解码而成。这些 Fixture 同时也是 Xcode Canvas 预览的 Development Assets(在 project.pbxproj 中配置了DEVELOPMENT_ASSET_PATHS)。

主力 Fixture 清单

Fixture 文件内容
movies.json电影库完整列表(约 1.6 MB,含多语言替代标题、海报)
series.json剧集库完整列表
movie-releases.json / series-releases.json可下载资源搜索结果
calendar-movies.json / calendar-episodes.json日历页上映数据
radarr-history.json / sonarr-history.json活动历史事件
root-folders.json、quality-profiles.json、disk-space.json、tags.json、system-status.json实例系统信息
movie-queue.json / series-queue.json下载队列任务

Mock 层还做了两个贴心设计(见 modifyCalendarMovies 等函数):

  • 模拟网络延迟:每个闭包内用Task.sleep睡 1–3 秒,让加载骨架屏、进度态都能被观察到
  • 日期实时化:把日历 Fixture 中的固定日期按“距今天数”动态平移,保证日历页永远有“即将上映”的内容

种子实例:seed-instances 本地联调

想连真实服务器又嫌配置麻烦?项目提供 seed-instances.example.json 模板:复制为seed-instances.json填入 URL 与 API Key 后,在 DEBUG 构建的 Settings → Instances 中使用Seed Instances即可批量导入实例(约定详见 CLAUDE.md 的 Local instances 一节)。

Xcode Preview:免费造出离线、失败、超时 3 种 UI 状态

Mock 的“闭包可替换”设计还有一个隐藏福利:每个#Preview里可以直接覆盖单个闭包,零成本造出各种异常 UI。MoviesView.swift 就是范例:

#Preview("Offline") { dependencies.api.radarr.fetch = { _ in throw API.Error.notConnectedToInternet } return ContentView().withAppState() }

同样的套路还覆盖了Failure(服务端错误)和Timeout(私网 IP 超时)场景——这正是“预览 Fixture + Mock 依赖”配合的威力:UI 设计师和开发者不需要连服务器就能看到所有状态。

自动化测试清单:Swift Testing 框架下的 20 个测试文件

Tests/ 目录共 20 个测试文件、约 3000 行断言,全部采用 Swift 官方新框架Swift Testing(import Testing+@Test+#expect)。测试 Target 为Tests.xctest,挂在共享 Scheme 中(见 Ruddarr.xcscheme)。

按主题分组的测试清单

测试文件验证目标
PlaceholderTests.swift本地化字符串模板%@占位符替换(内存安全版String(format:))
BreakableTests.swift资源标题长字符串换行插入零宽空格、最小尾缀保护
FormatStringParityTests.swift / HexEncodingTests.swiftSwift 6 格式化迁移的字节级等价性(以旧实现为 oracle 对照)
DateDecodingStrategyTests.swiftiso8601extended日期解码策略与旧ISO8601DateFormatter行为一致
LossyDecodingTests.swiftSonarr 返回"languages": [null]时数组丢空元素而非整体失败
DecodingErrorDiagnosticsTests.swift / InstanceDecodeDiagnosticsTests.swift解码失败诊断:Sentry 上报门控与无 PII 的取证摘要
InstanceWireFormatTests.swift锁死 iCloud 同步instances值的跨版本 JSON 格式,防止旧版本客户端把库写空
RouteTableTests.swift / NetworkInterfacesTests.swift / ResolverProbeTests.swift / InstanceResolverTests.swift网络路由表解析、LAN/Tailscale 分类、实例 URL 选择器状态机
RequestTimeoutTests.swift / RequestDiagnosticsTests.swift本地/远程差异化超时预算、失败请求环形缓冲
PrivacyTests.swift诊断报告中 URL 脱敏(掩码域名、保留子网段)
FormatBytesTests.swift / RootFolderLabelTests.swift / ChangelogParserTests.swift / RuddarrTests.swift字节格式化、根文件夹标签消歧、更新日志解析、本地化参数替换

三条值得学习的测试约定

  1. 纯函数优先:几乎每个测试都先测“纯逻辑”(路由、掩码、解析器),把系统依赖隔离到不可测的壳层里
  2. Oracle 对照法:重构时保留旧实现作参照(如 DateDecodingStrategyTests.swift 中的legacy函数),断言新旧输出字节级一致
  3. Fixture 冻结契约:用固化 JSON 锁死 iCloud 同步格式,注释中甚至写明了“为什么必须存在”(InstanceWireFormatTests.swift 头注释)

CI 三阶段流水线:ci_scripts 详解

ci_scripts/ 下三个 Shell 脚本挂在构建流程的“克隆后 / 构建前 / 构建后”三个钩子上。

克隆后:ci_post_clone.sh 同步多语言翻译

ci_post_clone.sh 负责把 Crowdin 平台上的翻译拉进仓库构建:

  • 校验CROWDIN_TOKEN,用 Homebrew 安装crowdin与yq(均带 5 次线性退避重试的retry包装)
  • 从 crowdin.yml 读取导出语言列表,并把 Crowdin 语言码转成 Apple 语言码(zh-CN→zh-Hans、es-ES→es)
  • macOS 平台用plutil动态写入Info.plist的CFBundleLocalizations
  • 用jq从三个.xcstrings字符串目录中删除未导出的语言,避免把半成品翻译打进发布包

构建前:ci_pre_xcodebuild.sh 注入发布密钥

ci_pre_xcodebuild.sh 做两件小事:跳过 SwiftLint 插件指纹校验,然后从密钥APNS_SECRET注入 Info.plist 的APNsKey(推送证书),并写入CI_BRANCH供版本标记使用——缺失任何一个都会让流水线快速失败。

构建后:ci_post_xcodebuild.sh 上传 dSYM

ci_post_xcodebuild.sh 安装sentry-cli并执行sentry-cli debug-files upload,把归档(CI_ARCHIVE_PATH)中的 dSYM 符号表上传到 Sentry,保证线上崩溃报告能还原 Swift 堆栈。

CI 实际执行的 xcodebuild 命令

按 CLAUDE.md 记录,CI 分别用以下命令跑 iOS 与 macOS 测试(均禁用代码签名):

# iOS 模拟器测试 xcodebuild test -scheme Ruddarr -skipPackagePluginValidation \ -destination 'platform=iOS Simulator,name=iPhone 17 Pro' \ CODE_SIGNING_ALLOWED=NO CODE_SIGN_IDENTITY="" CODE_SIGNING_REQUIRED=NO \ MACOSX_DEPLOYMENT_TARGET=26.0

macOS 侧把-destination换成platform=macOS即可;CodeQL 静态分析则用xcodebuild build加相同参数。

上手清单:5 分钟搭起本地验证环境

  1. 打开 Ruddarr.xcodeproj,选中共享 SchemeRuddarr
  2. 无需真实实例:取消注释 Ruddarr.swift 中的dependencies.api = .mock,跑起完全离线的数据
  3. 浏览 Ruddarr/Views/ 中任意带#Preview的文件,在 Canvas 中切换 Offline / Failure / Timeout 三个变体
  4. 需要联调真实服务器时,复制 seed-instances.example.json 填写后使用Seed Instances
  5. 本地跑一遍xcodebuild test -scheme Ruddarr(命令见上文),确认 20 个测试文件全绿

小结

Ruddarr 的测试体系可以浓缩为三句话:用闭包抽象 API、用 Fixture 喂数据、用 CI 卡质量。Mock 依赖让预览和调试彻底摆脱服务器,Preview Content 的 26 个 JSON 让假数据足够逼真,而 CI 的三阶段脚本加上 Swift Testing 自动化测试,则把“翻译同步、密钥注入、符号上传、行为回归”全部自动化。这套实践对任何依赖外部 API 的 iOS 项目都非常有参考价值。 🚀

【免费下载链接】app

A native iOS companion app for Radarr and Sonarr instances.

项目地址:https://gitcode.com/gh_mirrors/app154/app
点击查看免费下载
上一篇:Oh My Posh 功能更新解析:Accordion 折叠段、跨段模板属性与 PowerShell 迁移指南
下一篇:如何快速用好 Qwen3.8-27B 的图片理解:从数学题到扫描件解析的完整实战流水线

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进