【免费下载链接】app
A native iOS companion app for Radarr and Sonarr instances.
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.swift | Swift 6 格式化迁移的字节级等价性(以旧实现为 oracle 对照) |
| DateDecodingStrategyTests.swift | iso8601extended日期解码策略与旧ISO8601DateFormatter行为一致 |
| LossyDecodingTests.swift | Sonarr 返回"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 | 字节格式化、根文件夹标签消歧、更新日志解析、本地化参数替换 |
三条值得学习的测试约定
- 纯函数优先:几乎每个测试都先测“纯逻辑”(路由、掩码、解析器),把系统依赖隔离到不可测的壳层里
- Oracle 对照法:重构时保留旧实现作参照(如 DateDecodingStrategyTests.swift 中的
legacy函数),断言新旧输出字节级一致 - 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.0macOS 侧把-destination换成platform=macOS即可;CodeQL 静态分析则用xcodebuild build加相同参数。
上手清单:5 分钟搭起本地验证环境
- 打开 Ruddarr.xcodeproj,选中共享 Scheme
Ruddarr - 无需真实实例:取消注释 Ruddarr.swift 中的
dependencies.api = .mock,跑起完全离线的数据 - 浏览 Ruddarr/Views/ 中任意带
#Preview的文件,在 Canvas 中切换 Offline / Failure / Timeout 三个变体 - 需要联调真实服务器时,复制 seed-instances.example.json 填写后使用Seed Instances
- 本地跑一遍
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.
相关推荐
IBAnalyzer 单元测试之道:依赖注入、Stub 与 Mock 的完整实践清单
IBAnalyzer 单元测试之道:依赖注入、Stub 与 Mock 的完整实践清单 IBAnalyzer 是一款无需运行 App 即可检查 xib 与 sto
lolPing 桌面应用怎么测:测试与 CI 实践完整清单
lolPing 桌面应用怎么测:测试与 CI 实践完整清单 lolPing https://link.gitcode.com/i/bdf7b26b2f037ac
用 pytest Fixture 组合重构 FastAPI + SQLModel 测试:Client Fixture 与依赖覆盖的完整实践
用 pytest Fixture 组合重构 FastAPI + SQLModel 测试:Client Fixture 与依赖覆盖的完整实践 导读 本文基于 SQ
ORM数据库后端
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考