Kaset macOS 音乐播放器开发者指南Swift Testing 测试体系与 Mock 客户端实战教程【免费下载链接】kaset The missing YouTube and YouTube Music macOS app项目地址: https://gitcode.com/gh_mirrors/ka/kasetKaset 是一款用 Swift SwiftUI 打造的 YouTube 和 YouTube Music macOS 原生客户端开源免费。本文将从开发者视角拆解它如何基于Swift Testing搭建单元测试体系Suite/Test宏、#expect断言、参数化测试、测试标签以及MockYTMusicClient、MockYouTubeClient、MockURLProtocol三类 Mock 客户端的实战用法帮你快速上手这个 macOS 音乐播放器的工程化测试规范。为什么 Kaset 全面迁移到 Swift Testing Kaset 曾全面使用 XCTest随后依据架构决策记录 ADR-0006: Migration from XCTest to Swift Testing 分 8 个阶段完成迁移。迁移的核心理由更简洁的语法Test、Suite宏取代testXxx方法命名约定参数化测试Test(arguments:)让每组参数独立报告通过/失败标签体系.api、.parser、.service等自定义标签支持按分类过滤运行一流的 async/await 支持与更丰富的#expect()/#require()诊断信息默认并行执行测试套件跑得更快两个框架目前共存性能测试因 Swift Testing 尚无measure {}等价物保留在 XCTestUI 测试因XCUIApplication集成仍在成熟期也保留 XCTest。这是非常务实的渐进式迁移策略。测试目录结构一眼看懂工程布局 所有测试集中在Tests/目录下按被测模块分层Tests/ ├── KasetTests/ # 主应用单元测试Swift Testing │ ├── Helpers/ # Mock 客户端与测试工具 │ │ ├── MockYTMusicClient.swift # YouTube Music API 客户端 Mock │ │ ├── MockYouTubeClient.swift # 普通 YouTube API 客户端 Mock │ │ ├── MockURLProtocol.swift # 网络层 Mock │ │ └── TestFixtures.swift # 模型工厂 JSON 夹具加载 │ ├── SwiftTestingHelpers/ │ │ └── Tags.swift # 自定义测试标签定义 │ ├── Fixtures/ # 真实 API 响应的 JSON 夹具 │ │ ├── home_response.json │ │ └── YouTube/ # 脱敏后的普通 YouTube 夹具 │ └── *Tests.swift └── YouTubeAskCoreTests/ # YouTube Ask 解析内核的独立测试这个分层值得学习Helpers假实现与 Fixtures数据分离。Mock 客户端只负责决定返回什么JSON 夹具负责返回的真实数据长什么样两者正交组合覆盖了从纯逻辑到近乎端到端的各种测试粒度。日常测试命令CLI 优先 ⚡官方测试指南 docs/testing.md 明确了默认本地验证回路日常开发只需记住这几条# 只构建 swift build # 运行全部单元测试跳过 UI 测试 swift test --skip KasetUITests # 按名称过滤只跑解析器相关测试 swift test --skip KasetTests --filter Parser # 按标签过滤只跑 API 测试 / 排除慢测试 swift test --skip KasetUITests --filter .api swift test --skip KasetUITests --skip .slow # Lint 格式化 swiftlint --strict swiftformat .只有在 UI 调试、Scheme 级排查或 CI 一致性验证时才升级到 Xcodexcodebuild test -scheme Kaset -only-testing:KasetTests -skip-testing:KasetUITests 注意Swift Testing 的测试需要Xcode 16才能被发现CI 构建代理务必升级。Swift Testing 实战从模板到参数化 最小可运行测试套件Kaset 的测试文件统一遵循源文件同名约定如 YTMusicClient.swift →YTMusicClientTests.swift标准模板如下import Testing testable import Kaset Suite(MyService, .serialized, .tags(.service)) MainActor struct MyServiceTests { let sut: MyService let mockClient: MockYTMusicClient init() { mockClient MockYTMusicClient() sut MyService(client: mockClient) } Test(Does something correctly) func doesSomething() async throws { mockClient.homeResponse HomeResponse(sections: [], continuationToken: nil) let result try await sut.doSomething() #expect(result ! nil) } }三个关键点新手最容易踩坑细节说明init()替代setUp()Swift Testing 每个测试都会新建 struct 实例不需要tearDown()ARC 自动清理.serializedMainActor套件必须串行否则并行执行会产生竞态.timeLimit()给异步测试加超时如.timeLimit(.minutes(1))防止测试挂死断言速查表XCTest → Swift TestingXCTestSwift TestingXCTAssertEqual(a, b)#expect(a b)XCTAssertTrue(x)#expect(x)XCTAssertNil(x)#expect(x nil)XCTAssertThrowsError#expect(throws: Error.self) { try x }XCTUnwrap(x)let x try #require(x)XCTFail(msg)Issue.record(msg)完整映射见 ADR-0006 断言参考表。参数化测试一份逻辑多组数据以真实的 HomeResponseParserTests.swift 为例解析器测试用arguments:让每个输入用例独立出报告Test(Duration formatting, arguments: [ (0.0, 0:00), (65.0, 1:05), (3661.0, 1:01:01), ]) func durationFormatting(seconds: Double, expected: String) { let song TestFixtures.makeSong(duration: seconds) #expect(song.durationDisplay expected) }比 XCTest 里for循环遍历用例列表更清晰——某一组参数失败时你能直接定位到具体是哪组数据。Mock 客户端实战不碰真实网络 ️这是 Kaset 测试体系最有价值的部分。整个Services/API层基于协议设计YTMusicClientProtocol、YouTubeClientProtocol见 ADR-0002: Protocol-based ServicesMock 只是协议的可配置替身。MockYTMusicClient给 ViewModel 喂数据MockYTMusicClient.swift 实现了YTMusicClientProtocol每个端点对应一个可写属性// 测试代码先配置 Mock再驱动被测对象 let mockClient MockYTMusicClient() mockClient.homeResponse HomeResponse(sections: [makeSection()], continuationToken: nil) mockClient.error nil // 或设置任意 Error 验证失败路径 let viewModel HomeViewModel(client: mockClient) await viewModel.load() #expect(!viewModel.isLoading) #expect(!viewModel.sections.isEmpty)它的设计细节值得注意实例属性而非 static——Swift Testing 默认并行执行static var会导致套件间竞态。项目铁律Mock 中禁止static var状态走实例属性或init()传入精细到分页如searchContinuationResponses按 token 区分翻页响应historyResponseSequence支持按调用次序返回不同结果错误注入error属性一设置所有调用即抛出一条语句覆盖异常分支MockYouTubeClient普通 YouTube 源的替身MockYouTubeClient.swift 额外提供了调用追踪能力homeFeedCallCount、lastSearchQuery等private(set)计数器让测试可以断言某次操作恰好只请求了首页一次或搜索时携带了正确的筛选器——这在验证防抖、去重、单飞行single-flight类逻辑时非常有用。MockURLProtocol网络层拦截需要测试真实URLSession代码路径而不只是协议层时MockURLProtocol.swift 继承URLProtocol拦截所有请求MockURLProtocol.requestHandler { request in let data TestFixtures.loadJSON(home_response) // 加载 JSON 夹具 let response HTTPURLResponse(url: request.url!, statusCode: 200, httpVersion: nil, headerFields: nil)! return (response, data) }新版代码推荐用makeMockSession(handler:)按 Session ID 隔离 handler避免并行套件互相覆盖——这正是并行执行时代 Mock 设计演进的缩影。TestFixtures模型工厂 夹具加载TestFixtures.swift 提供两类工具模型工厂TestFixtures.makeSong(duration: 180)一行生成带默认值、可局部覆写的模型JSON 夹具TestFixtures.loadJSON(home_response)从 Tests/KasetTests/Fixtures/ 加载录制过的真实 API 响应如home_response.json、search_response.json用于解析器测试夹具安全红线来自 docs/testing.mdYouTube Ask 类夹具必须手工编写、占位符化如fixture-video-a严禁把 Cookie、API Key、账号标识、个性化载荷复制进源码或测试输出。测试标签给测试分类管理 ️Tags.swift 定义了 7 个自定义标签形成一套测试分类语言标签覆盖范围典型文件.api网络请求、重试策略YTMusicClientTests、RetryPolicyTests.parser响应解析HomeResponseParserTests.viewModelViewModel 逻辑HomeViewModelTests.service服务层AuthServiceTests、PlayerServiceTests.model数据模型ModelTests、LikeStatusTests.slow耗时 1s 的测试性能/集成测试.integration多组件集成如 Apple IntelligenceMusicIntentIntegrationTests标签与 CI 配合形成稳定流水线CI 常跑任务排除.integrationLLM 输出不确定、需要 Apple Intelligence 环境集成测试放到定时任务中单独运行从根上消除 CI 抖动。新手上手清单 ✅新建测试文件命名与被测源文件对应放入Tests/KasetTests/用Suite Test模板编写MainActor套件务必加.serialized需要数据就用TestFixtures工厂需要网络行为就配置 Mock 客户端属性用标签声明分类本地用--filter快速迭代提交前跑swift test --skip KasetUITestsswiftlint --strict swiftformat .硬性要求Sources/Kaset/下 Services、Models、ViewModels、Utilities 的任何新代码都必须附带单元测试——这是 docs/testing.md 明确的贡献规范。延伸阅读 完整测试指南docs/testing.mdXCTest 迁移决策全文docs/adr/0006-swift-testing-migration.md协议化服务设计Mock 可行的前提docs/adr/0002-protocol-based-services.mdMock 客户端实现Tests/KasetTests/Helpers/MockYTMusicClient.swift测试标签定义Tests/KasetTests/SwiftTestingHelpers/Tags.swift架构总览docs/architecture.md【免费下载链接】kaset The missing YouTube and YouTube Music macOS app项目地址: https://gitcode.com/gh_mirrors/ka/kaset创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考