首页
/ ECC Swift Testing 规则详解:用 Swift Testing 编写隔离、参数化与可注入的确定性测试

ECC Swift Testing 规则详解:用 Swift Testing 编写隔离、参数化与可注入的确定性测试

2026-09-06 12:30:24作者:裘旻烁

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.mdalwaysApply: true,全局常驻)对 Swift 项目同样生效,包含以下硬性要求:

最低测试覆盖率 80%,且三类测试全部必须覆盖:

  1. 单元测试——单个函数、工具、组件;
  2. 集成测试——API 端点、数据库操作;
  3. 端到端测试——关键用户流(框架按语言选择,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 querythrows 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 in deinit. 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 对并发被测对象(actorasync 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-guide agent 按 common 基线强制"先写测试"的 RED-GREEN-REFACTOR 流程,Swift 侧的测试写法(@Test#expect、隔离、参数化)则按本篇规则约束;
  • 兜底:涉及文件系统/网络等外部依赖的测试设计时,Reference 指向的 swift-protocol-di-testing 提供协议划分与 mock 的完整范式;仓库内还有 swift-concurrency-6-2swift-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 一条命令闭环验证覆盖率门槛。

登录后查看全文
热门项目推荐
相关项目推荐