若要让 Xcode 高亮显示调用共享 XCTest 辅助函数的测试,请为辅助函数设置默认的 #filePath 和 #line 参数,并将二者传递给断言。本指南将介绍修复方法、嵌套辅助函数以及 Swift Testing 中的对应做法。

为什么 Xcode 会高亮显示辅助函数?

共享断言会引入另一个源代码位置。测试调用你的辅助函数,而辅助函数调用 XCTest 断言。如果该断言使用默认的位置参数,报告的位置就是辅助函数内部的断言。Apple 在 XCTAssertEqual 和 XCTFail 中都记录了 file: StaticString = #filePath 和 line: UInt = #line。

在辅助函数的边界处捕获调用者的位置,然后将这些值传递下去。Swift 的源代码位置默认值会捕获调用点;Swift 项目在 SE-0274:简洁的魔术文件名 中解释了这一行为。Apple 还在 迁移到 Swift Testing 中展示了这一确切的 XCTest 辅助函数模式,其中 Xcode 将失败归因于辅助函数调用。

对于在 Mac 上使用 Xcode 的开发者,具体目标是保留可复用的检查,同时确保每个失败都能指向相关的测试输入。下面的示例只使用小型整数数组。运行时不需要应用模型、服务替身、下载的依赖项或网络访问。

为 XCTest 辅助函数添加调用者位置参数

使用现有的 macOS 单元测试目标。如果项目需要创建一个目标,请选择 File > New > Target,筛选 Test,然后选择 Unit Testing Bundle。要添加测试文件,请选择 File > New > New File From Template,然后选择 XCTest Unit Test。这些是 Apple 在将测试添加到 Xcode 项目中记录的路径。

将下面这个完整的测试文件放入该单元测试目标。第二个测试故意提供了错误的预期总和,方便你查看失败位置。

import XCTest

private func assertTotal(
    _ values: [Int],
    equals expected: Int,
    file: StaticString = #filePath,
    line: UInt = #line
) {
    let actual = values.reduce(0, +)
    XCTAssertEqual(
        actual,
        expected,
        "Unexpected total for \(values)",
        file: file,
        line: line
    )
}

final class TotalHelperTests: XCTestCase {
    func testMatchingTotal() {
        assertTotal([2, 3, 4], equals: 9)
    }

    func testDeliberatelyIncorrectTotal() {
        assertTotal([2, 3, 4], equals: 10)
    }
}

关键在于参数类型、默认值和显式传递。StaticString 和 UInt 与 XCTest 的断言签名匹配。请将 #filePath 和 #line 直接写在辅助函数的默认参数中。这样,测试调用辅助函数时就不必手动提供任一位置参数。请参阅文档中的 XCTAssertEqual 参数。

这里的运算仅作示例:2 + 3 + 4 等于 9,因此第一个比较结果匹配,第二个则不匹配。我们使用 Python 在本地检查了这些算术示例。研究环境中没有 Swift 可执行程序,因此我们检查了 Swift 示例,但未在此处编译或运行。

验证位置和测试结果

选择 Product > Test,或者使用测试声明旁边的播放控件运行单个测试。完成后,在 Test navigator 中选择失败的测试并查看诊断信息。Apple 在运行测试并解读结果中介绍了这些控件和失败详情。

对于此示例,请分别验证两个结果:testDeliberatelyIncorrectTotal() 失败,并且其断言诊断信息指向 assertTotal([2, 3, 4], equals: 10) 调用。预期的调用者归因方式遵循了 Apple 展示的参数传递模式。检查后,将 10 改为 9 并重新运行测试;完成后的测试套件不应保留这个故意设置的不匹配。

如果诊断信息仍指向辅助函数内部,请检查最终的断言调用。如果辅助函数在记录失败时没有传入 file: file 或 line: line,仅声明位置参数是不够的。

在每一层辅助函数中保留位置信息

嵌套辅助函数也需要显式传递位置。假设测试调用一个领域辅助函数,而该函数又调用更底层的断言辅助函数。如果外层辅助函数遗漏了位置参数,底层辅助函数就会捕获这个中间调用点。

这是一个值得检查的语言边界:源代码位置默认值不会自动穿过任意的辅助函数调用。SE-0285:简化简洁魔术文件字符串的过渡明确讨论了这些默认值不会传递给下一层的行为。

下面是一个完整的替代测试文件,展示了辅助函数如何调用另一个辅助函数,同时保留原始测试位置。

import XCTest

private func assertIntegerEqual(
    _ actual: Int,
    _ expected: Int,
    file: StaticString = #filePath,
    line: UInt = #line
) {
    XCTAssertEqual(actual, expected, file: file, line: line)
}

private func assertRowTotal(
    _ row: [Int],
    equals expected: Int,
    file: StaticString = #filePath,
    line: UInt = #line
) {
    assertIntegerEqual(
        row.reduce(0, +),
        expected,
        file: file,
        line: line
    )
}

final class NestedTotalHelperTests: XCTestCase {
    func testRowTotal() {
        assertRowTotal([2, 3, 4], equals: 9)
    }
}

审查较长的辅助函数调用链时,可以进行简单的追踪练习:从测试调用开始,逐一跟进每个辅助函数,确认同一个捕获的位置最终传递到了断言。应在预期显示在结果中的边界处捕获默认值;其余层级则传递已有的值。不要把位置捕获藏在另一个用于默认参数的函数后面,这是 SE-0285 所示的常见陷阱。

报告失败会停止调用它的测试吗?

失败归因和控制流是两个独立的决定。XCTest 的 continueAfterFailure 默认值为 true,因此断言失败后通常仍会继续执行。更改报告的位置不会改变这一点。Apple 在 continueAfterFailure 中记录了这一行为。

如果辅助函数发现某个条件导致其余工作无效,请为该辅助函数提供明确的退出路径。例如,使用传入的位置记录失败,然后从辅助函数返回。请记住,从辅助函数返回时,控制权会回到调用者;应据此安排调用者后续的工作。Apple 的迁移指南展示了先无条件报告失败、再返回的做法。

首次验证时应保持同步且简单。在添加更复杂的设置或异步行为之前,先确认一个故意设置错误的计算会在预期的调用点失败。这样可以将位置传递问题与其他测试执行问题区分开来。

在 Swift Testing 中使用受支持的源代码位置

对于由 Swift Testing 测试调用的辅助函数,请将 #expect 与显式的 SourceLocation 配合使用。Apple 将 sourceLocation 记录为期望和问题的归因位置,并说明条件为假时,会为当前任务中正在运行的测试记录一个问题。请参阅 expect(_:_:sourceLocation:)。

下面这个完整的替代文件需要 Swift Testing,该框架随 Xcode 16 和 Swift 6.0 提供。它将四个位置组成部分分别捕获为默认值,然后构造文档中所述的 SourceLocation 初始化器。

import Testing

private func expectTotal(
    _ values: [Int],
    equals expected: Int,
    fileID: String = #fileID,
    filePath: String = #filePath,
    line: Int = #line,
    column: Int = #column
) {
    let location = SourceLocation(
        fileID: fileID,
        filePath: filePath,
        line: line,
        column: column
    )
    let actual = values.reduce(0, +)
    #expect(actual == expected, sourceLocation: location)
}

@Test func matchingTotal() {
    expectTotal([2, 3, 4], equals: 9)
}

@Test func deliberatelyIncorrectTotal() {
    expectTotal([2, 3, 4], equals: 10)
}

初始化器要求文件 ID 格式正确且非空,并且行号和列号都必须为正数。捕获编译器提供的组成部分可以避免自行编造这些值。嵌套这些辅助函数时,请传递全部四个已捕获的参数,就像 XCTest 版本传递两个参数一样。同样,验证归因后,请修正故意设置的 10 预期值。

XCTest 和 Swift Testing 共用辅助函数时会有什么变化?

这两个框架可以共存于同一个测试包中,但跨越框架边界的辅助函数需要根据版本进行处理。Apple 当前的迁移指南记录了互操作性及其默认设置:Swift 6.4 之前的工具链使用 none;Swift 6.4 或更高版本中,声明较早工具版本的包默认使用 limited,而声明工具版本为 6.4 或更高的包默认使用 complete。

互操作模式 Swift Testing 测试中的 XCTest 断言失败
none 忽略跨库问题
limited 报告为警告,并提供现代化建议
complete 保留失败严重级别,并提供现代化建议
strict 使用 fatalError 报告

这些模式解释了为什么辅助函数可能报告了正确的文本,但 Swift Testing 测试仍然通过。Apple 的 WWDC26 迁移讲座展示了这一确切结果,随后将测试计划中 Test Execution 下的互操作设置改为 Complete,以保留失败严重级别。依赖此功能之前,请检查已安装的工具链。

对于较旧的工具链,应让每个框架使用自己的原生断言报告方式,并在底层共享确定性计算。在受支持的互操作配置中,Apple 还展示了将辅助函数中的 XCTFail 替换为 Swift Testing 的 Issue.record。迁移时仍需保留源代码位置的传递。

诊断剩余问题

观察到的情况 下一步检查内容
失败指向断言辅助函数内部 确认断言收到了捕获的位置参数
失败指向外层辅助函数内部 确认外层辅助函数传递了调用者的位置
XCTest 断言失败,但 Swift Testing 测试仍通过 检查工具链支持情况和互操作模式
XCTest 断言失败后代码仍在运行 检查预期控制流和 continueAfterFailure
运行中看不到示例测试 确认文件属于单元测试目标,且当前测试计划包含该文件

对于测试发现和选择,Apple 的测试设置指南和测试执行指南介绍了测试目标和当前测试计划。对于跨框架的通过结果,请先依据上文记录的互操作规则进行检查,再更改位置参数。

实用的共享辅助函数会保留三项内容:有意义的比较、指向预期调用者的诊断信息,以及预期的测试结果。使用一个故意设置的不匹配来验证这些内容,随后移除该不匹配;每增加一层辅助函数,都要继续传递捕获的位置。

来源