ECC Swift Testing 规则详解:用 Swift Testing 编写隔离、参数化与可注入的确定性测试
ECC 将面向 AI 编程助手(Cursor、Claude Code 等)的工程规范沉淀为一套可被按需加载的规则文件(rules),其中 Swift Testing 规则 就是这条链上针对 Swift 项目的测试规范:它继承通用测试基线(80% 覆盖率、TDD 流程),并规定了 Swift Testing 框架下 @Test / #expect 的写法、测试隔离方式、参数化测试与覆盖率采集方法。读完本文,你能理解该规则在 ECC 规则体系中的加载机制,掌握规则中每一类测试写法的完整代码范式,并能结合仓库中配套的 swift-protocol-di-testing 技能,落地一套基于协议依赖注入的 Swift 测试方案。
规则文件定位:Cursor 按需加载的语言专属扩展
该规则以 Cursor Rules 的 Frontmatter + Markdown 格式定义在 .cursor/rules/swift-testing.md,头部元数据声明了它的行为:
---
description: "Swift testing extending common rules"
globs: ["**/*.swift", "**/Package.swift"]
alwaysApply: false
---
三个字段决定了这条规则的工作方式:
globs: ["**/*.swift", "**/Package.swift"]:当 Cursor 上下文涉及任意.swift文件或 SwiftPM 的Package.swift清单时,该规则会被自动纳入上下文;alwaysApply: false:它不是全局常驻规则,只在上述文件出现时生效,避免污染其他技术栈会话的上下文预算;description:供检索/匹配使用的简短描述。
规则正文第一行即声明其定位:This file extends the common testing rule with Swift specific content——它是对 通用测试规则 的 Swift 特化扩展。值得注意的是,仓库中同一规范还存在一个不带 Cursor 专属 Frontmatter 的"源版本" rules/swift/testing.md,两者内容一致,后者用 paths 字段(同样匹配 **/*.swift 与 **/Package.swift)声明作用范围。从源码结构看,.cursor/rules/ 目录是按"语言 + 维度"组织的规则集合(每个语言包含 coding-style、hooks、patterns、security、testing 五个维度文件,Swift 亦如此),而 scaffolds/cursor/ 目录则存放了 Cursor 侧的配套模板(如 hooks.json),供初始化 Swift 项目的工作区时复用整套规则。
这种"通用基线 + 语言扩展"的分层设计是 ECC 规则体系的核心组织原则:语言文件只写增量,公共要求(覆盖率门槛、TDD 流程)统一维护在 common 层,改一处即全局生效。
继承的通用测试基线:80% 覆盖率与强制 TDD
.cursor/rules/swift-testing.md 声明"extends common testing rule",而该基线(.cursor/rules/common-testing.md,alwaysApply: true,全局常驻)对 Swift 项目同样生效,包含以下硬性要求:
最低测试覆盖率 80%,且三类测试全部必须覆盖:
- 单元测试——单个函数、工具、组件;
- 集成测试——API 端点、数据库操作;
- 端到端测试——关键用户流(框架按语言选择,Swift 场景通常由 Swift Testing 的 async 测试承担)。
强制 TDD 工作流(六步):
1. 先写测试(RED)
2. 运行测试——应当失败
3. 写最小实现(GREEN)
4. 运行测试——应当通过
5. 重构(IMPROVE)
6. 验证覆盖率(80%+)
测试失败排查顺序:使用 tdd-guide agent(对应仓库中的 agents/tdd-guide.md,要求在新功能开发时 PROACTIVELY 启用)→ 检查测试隔离 → 校验 mock 是否正确 → 修实现而非修测试(除非测试本身写错)。
与 Swift 规则配套的 rules/common/testing.md 还补充了 AAA 结构与命名约定,直接适用于 Swift 测试的编排:
- Arrange-Act-Assert:每个测试显式分三段——构造前置条件、执行被测行为、断言结果;
- 行为描述式命名:测试名解释"在什么条件下发生什么行为",例如
returns empty array when no markets match query、throws error when API key is missing。这正好与 Swift 规则中@Test("User creation validates email")这种字符串描述符风格一致——@Test的第一个参数即行为描述,承担命名规范的作用。
理解这一层的关系很重要:.cursor/rules/swift-testing.md 本身只写"Swift 怎么写",而"必须写、写到什么程度、按什么流程写"由 common 基线保证。
核心内容一:Swift Testing 框架的 @Test 与 #expect
规则的第一个小节明确:新测试一律使用 Swift Testing(import Testing),使用 @Test 宏与 #expect 宏,而非 XCTest。规则给出的标准范式是异常路径断言:
@Test("User creation validates email")
func userCreationValidatesEmail() throws {
#expect(throws: ValidationError.invalidEmail) {
try User(email: "not-an-email")
}
}
要点解析:
@Test("...")中的字符串是测试的行为描述,对应 common 基线里的"行为描述式命名"要求;#expect(throws:)是宏形式的断言,throws:参数直接匹配具体错误值(如ValidationError.invalidEmail),比 XCTest 的XCTAssertThrowsError写法更贴近 Swift 的错误类型系统;- 测试函数标注
throws后,try直接作用于被测调用,无需额外包装。
这一选择意味着仓库规则要求新代码统一迁移到 Swift Testing 生态:它原生支持 Swift 并发下的 async 测试、await #expect(...) 写法,以及后文要讲的参数化与隔离特性。
核心内容二:测试隔离——init 建立、deinit 销毁
规则对隔离性给出了一条明确的实现约定:
Each test gets a fresh instance — set up in
init, tear down indeinit. No shared mutable state between tests.
即:每个测试拿到全新实例,在 init 中完成搭建、在 deinit 中完成拆除,测试之间禁止共享可变状态。 这条约定与 common 基线中"测试失败先检查隔离(Check test isolation)"的排查项形成闭环——隔离失效是共享状态污染、顺序依赖导致的偶发失败的最常见来源。
从规则原文看,它描述的是针对"测试主体(SUT)包装类"这一常见模式:将 SUT 及其依赖封装为测试 fixture 类,每个 @Test 函数内部构造该类的实例,利用 Swift 的引用计数保证 deinit 在函数结束时自动释放资源(如关闭 mock 连接、清理临时目录)。这与 Swift Testing 的运行模型匹配:同一测试套件内各 @Test 函数相互独立、可并行执行,任何静态可变状态都会破坏可重复性。
核心内容三:参数化测试的 arguments 写法
规则给出了参数化(data-driven)测试的标准形式:
@Test("Validates formats", arguments: ["json", "xml", "csv"])
func validatesFormat(format: String) throws {
let parser = try Parser(format: format)
#expect(parser.isValid)
}
其中 @Test 宏的 arguments: 参数声明了一组测试数据,测试框架会为每个元素生成一个独立的测试实例:"json"、"xml"、"csv" 各跑一次 validatesFormat,任一数据项失败都会精确定位到该值。相比在函数体内手写 for 循环遍历数据集合,参数化的优势在于:
- 每个数据项在测试报告中是独立条目,失败可直接看到是哪个 format 出了问题;
- 数据项之间无状态耦合,符合上文"禁止共享可变状态"的隔离要求;
- 数据集合可以来自
Collection,扩展成多参数组合时框架会做笛卡尔积展开(规则示例保持单参数形式,多参数场景可从源码结构看按同样的arguments:机制叠加)。
对于 Swift 项目中"同一逻辑对不同输入格式/配置应表现一致"这类校验(规则示例即解析器格式验证),这是推荐的默认写法。
核心内容四:覆盖率采集
规则给出的覆盖率命令是 SwiftPM 原生能力:
swift test --enable-code-coverage
该命令在项目根目录(含 Package.swift 的包)下执行,跑完测试后在 .build/x86_64-unknown-linux-gnu/debug/ 或 .build/apple/Products/Debug/(macOS)目录下生成 codecov 风格的 .profdata / LLVM coverage 产物,可配合 Xcode 的覆盖率报告或 llvm-cov 工具链转换为可读报告。结合 common 基线的 80% 门槛,Swift 项目的验证链路即:swift test --enable-code-coverage 跑全量测试 → 生成覆盖率数据 → 确认 ≥80% 且三类测试齐备。规则将 **/Package.swift 列入 glob 匹配,正是因为 SwiftPM 包是该命令的触发前提——上下文里出现 Package.swift 即表示这是一个可用该命令闭环的 Swift 包。
纵深扩展:规则引用的 swift-protocol-di-testing 技能
规则末尾的 Reference 一节指向仓库内的 swift-protocol-di-testing 技能:protocol-based dependency injection and mock patterns with Swift Testing。该技能是 Swift Testing 规则在"外部依赖如何 mock"这一关键问题上的完整落地,核心是把文件系统、网络、外部 API 抽象为小而专注的协议,用默认参数注入生产实现、测试注入 mock,实现零 I/O 的确定性测试。其完整流程分五步:
1. 定义单一职责协议
每个协议只处理一类外部关注点,并且因为要跨 actor 边界使用而要求 Sendable:
// 文件系统访问
public protocol FileSystemProviding: Sendable {
func containerURL(for purpose: Purpose) -> URL?
}
// 文件读写操作
public protocol FileAccessorProviding: Sendable {
func read(from url: URL) throws -> Data
func write(_ data: Data, to url: URL) throws
func fileExists(at url: URL) -> Bool
}
2. 生产默认实现
public struct DefaultFileAccessor: FileAccessorProviding {
public func read(from url: URL) throws -> Data {
try Data(contentsOf: url)
}
public func write(_ data: Data, to url: URL) throws {
try data.write(to: url, options: .atomic)
}
public func fileExists(at url: URL) -> Bool {
FileManager.default.fileExists(atPath: url.path)
}
}
3. 可配置错误的 Mock 实现
Mock 的关键设计是可注入的错误属性,用于确定性触发失败路径(规则示例中的 #expect(throws:) 断言正是消费这些错误):
public final class MockFileAccessor: FileAccessorProviding, @unchecked Sendable {
public var files: [URL: Data] = [:]
public var readError: Error?
public var writeError: Error?
public func read(from url: URL) throws -> Data {
if let error = readError { throw error }
guard let data = files[url] else {
throw CocoaError(.fileReadNoSuchFile)
}
return data
}
// write / fileExists 同理
}
4. 默认参数注入
生产代码零改动即可用真实实现,测试只需显式传 mock:
public actor SyncManager {
public init(
fileSystem: FileSystemProviding = DefaultFileSystemProvider(),
fileAccessor: FileAccessorProviding = DefaultFileAccessor()
) { ... }
}
5. 用 Swift Testing 断言
import Testing
@Test("Sync manager handles missing container")
func testMissingContainer() async {
let mockFileSystem = MockFileSystemProvider(containerURL: nil)
let manager = SyncManager(fileSystem: mockFileSystem)
await #expect(throws: SyncError.containerNotAvailable) {
try await manager.sync()
}
}
注意 await #expect(throws:) 的 async 形态:Swift Testing 对并发被测对象(actor、async throws)提供原生支持,这正是规则要求新测试使用 import Testing 而非 XCTest 的实际收益之一。
该技能同时给出了边界清晰的最佳实践与反模式清单,可直接作为 Code Review 检查项:
- 每个协议只管一件事,禁止"上帝协议";
- 跨 actor 边界必须
Sendable; - 生产走默认参数,仅测试显式注入 mock;
- 只 mock 边界(文件系统、网络、外部 API),不要 mock 无外部依赖的内部类型;
- 避免用
#if DEBUG条件编译代替正规依赖注入。
规则如何被 Agent 消费:从规则到工作流
这套 Swift 规则并非孤立存在,而是 ECC"规则 + 代理 + 命令"协同体系中面向 Swift 的一环:
- 触发:在 Cursor 中打开任何
.swift/Package.swift文件时,swift-testing规则因globs匹配而进入上下文;common 测试规则因alwaysApply: true始终在场,两者叠加生效; - 执行:
tdd-guideagent 按 common 基线强制"先写测试"的 RED-GREEN-REFACTOR 流程,Swift 侧的测试写法(@Test、#expect、隔离、参数化)则按本篇规则约束; - 兜底:涉及文件系统/网络等外部依赖的测试设计时,Reference 指向的 swift-protocol-di-testing 提供协议划分与 mock 的完整范式;仓库内还有 swift-concurrency-6-2、swift-actor-persistence 等技能可与 actor 测试场景互补。
速查小结
| 要求 | 出处 | Swift 落地方式 |
|---|---|---|
| 新测试统一 Swift Testing | swift-testing.md | import Testing + @Test + #expect |
| 行为描述式命名 | common-testing.md / rules/common/testing.md | @Test("User creation validates email") |
| 测试隔离 | swift-testing.md | 每测试全新实例:init 搭建、deinit 拆除,禁止共享可变状态 |
| 参数化测试 | swift-testing.md | @Test("...", arguments: [...]) 逐数据项独立执行 |
| 覆盖率 ≥80% | common-testing.md | swift test --enable-code-coverage |
| 强制 TDD 六步 | common-testing.md | RED → GREEN → REFACTOR → 验证覆盖率 |
| 外部依赖 Mock | swift-protocol-di-testing 技能 | 单职责 Sendable 协议 + 默认参数注入 + 可配置错误的 Mock |
综合来看,.cursor/rules/swift-testing.md 的价值不在于单独的四小节内容,而在于它与 common 基线、tdd-guide agent 和 DI 测试技能构成的完整闭环:common 层规定"必须写、按 TDD 写、覆盖 80%",Swift 层规定"用 Swift Testing 这样写",技能层规定"外部依赖这样 mock"。在 Swift/SwiftPM 项目中启用 ECC 这套 Cursor 规则后,AI 助手产出的测试代码会被约束在这一范式内——新测试一律 @Test/#expect、数据驱动、无共享状态、异常路径经 mock 确定性触发,并可用 swift test --enable-code-coverage 一条命令闭环验证覆盖率门槛。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0624
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00