WWDC Quick Look 💓 By SwiftGGTeam
Triage test failures with XCTIssue

Triage test failures with XCTIssue

观看原视频

Highlight

Xcode 12 将 XCTest 失败封装为 XCTIssue,让失败携带类型、详细描述、底层错误、附件和调用栈,开发者能在 Issue Navigator 与测试报告里追到触发路径。

核心内容

维护测试套件最耗时间的环节,常常出现在测试已经失败之后。失败在本机或 CI 里出现时,开发者要回答四个问题:哪里失败,怎样失败,为什么失败,以及失败到底从哪一行测试代码触发。Xcode 12 把这些问题放进了同一条排查路径。

演讲里的例子来自一个叫 PlayGarden 的小项目。测试代码为了减少重复,被抽成了共享工具函数。问题也随之出现:失败标记落在 helper 里面时,测试方法本身只显示灰色标记,开发者还得顺着调用关系找上下文。

Xcode 12 的变化,是把失败、源码位置和调用栈一起呈现。Issue Navigator 能展开测试代码里的调用栈;测试报告也能显示同一组 frame,并通过 Jump 或 Assistant 按钮把报告和源码并排打开。对于来自 CI 的 result bundle,这能减少本地复现前的盲查时间。

底层 API 也被调整。过去 XCTest 记录失败时使用消息、文件路径、行号和是否为预期失败这四个离散值。Xcode 12 用 XCTIssue 包装这些数据,并加入类型、详细描述、底层错误和附件。自定义断言、失败观察、失败过滤和诊断附件,都可以围绕同一个对象完成。

详细内容

Swift 抛错测试拥有源码位置

04:21)XCTest 支持测试函数直接 throw。当测试抛出错误时,错误会形成失败消息。Xcode 12 结合 Swift runtime、iOS 和 tvOS 13.4、macOS 10.15.4 的改进,为这类错误报告源码位置。

05:15)同一组便利也进入了生命周期方法:setUpWithError() 在原来的 setUp() 前运行,tearDownWithError() 在原来的 tearDown() 后运行。新建测试文件模板会包含这些方法。旧方法仍可保留,常见原因是继承结构还依赖它们。

XCTIssue 编写自定义断言

09:52)自定义断言可以直接创建 XCTIssue,把失败描述、附件和调用位置写进同一个对象,再交给 record(_:)

func assertSomething(about data: Data,
                         file: StaticString = #filePath,
                         line: UInt = #line) {

        // Call out to custom validation function.
        if !isValid(data) {

            // Create issue, declare with var for mutability.
            var issue = XCTIssue(type: .assertionFailure, compactDescription: "Invalid data")

            // Attach the invalid data.
            issue.add(XCTAttachment(data: data))

            // Capture the call site location as the point of failure.
            let location = XCTSourceCodeLocation(filePath: file, lineNumber: line)
            issue.sourceCodeContext = XCTSourceCodeContext(location: location)

            // Record the issue.
            self.record(issue)
        }
    }

关键点:

  • var issue 允许后续修改 XCTIssue
  • XCTIssue(type:compactDescription:) 记录失败类型和短描述。
  • issue.add(XCTAttachment(data: data)) 把失败数据附到测试报告。
  • XCTSourceCodeLocation 使用调用方传入的 fileline,让报告指向调用自定义断言的位置。
  • self.record(issue) 将 issue 交给 XCTest 和 Xcode。

在测试类里观察失败

11:12record(_:) 是测试类里所有失败经过的入口。覆盖它可以在一个类范围内观察失败,然后继续交给 XCTest。

override func record(_ issue: XCTIssue) {

    // Observe, introspect, log, etc.:
    if shouldLog(issue) {
        print("I just observed an issue!")
    }

    // Don't forget to call super!
    super.record(issue)
}

关键点:

  • override func record(_ issue: XCTIssue) 让当前测试类接管失败记录流程。
  • shouldLog(issue) 代表本地的观察或日志条件。
  • super.record(issue) 必须调用,否则 issue 不会继续进入记录链。
  • 如果只想观察某一个测试类的失败,这种方式比全局观察更局部。

按条件抑制失败

11:30)覆盖 record(_:) 后,不调用 super 就会阻止 issue 被记录和上报。

override func record(_ issue: XCTIssue) {

    // If you don't want to record it, just return.
    if shouldSuppress(issue) {
        return
    }

    // Otherwise pass it to super.
    super.record(issue)
}

关键点:

  • shouldSuppress(issue) 判断当前失败是否要被过滤。
  • return 会结束记录流程,Xcode 不会收到这个 issue。
  • 未被过滤的失败仍然要调用 super.record(issue)
  • 这类逻辑适合放在边界清晰的测试类中,避免隐藏真正需要修复的问题。

给失败追加诊断附件

11:39)修改 issue 是覆盖 record(_:) 的常见用途。先把参数重新声明为 var,再添加附件,最后传给 super

override func record(_ issue: XCTIssue) {

    // Redeclare using var to enable mutation.
    var issue = issue

    // Add a simple attachment.
    issue.add(XCTAttachment(string: "hello"))

    // Pass it to super.
    super.record(issue)
}

关键点:

  • var issue = issue 将传入对象变成可修改副本。
  • issue.add(XCTAttachment(string: "hello")) 演示给失败添加字符串附件。
  • transcript 提到附件 API 支持多种类型,可用于保存自定义诊断数据。
  • 修改完成后调用 super.record(issue),让增强后的 issue 出现在 Xcode 测试报告里。

调用栈让共享 helper 的失败更清楚

08:28)共享 helper 里有多个 assertion 时,只传入一个调用位置仍然会留下歧义。XCTIssue 捕获并符号化调用栈后,测试方法可以显示灰色标记,真正失败的 helper 行显示红色标记,Issue Navigator 和测试报告都能沿 frame 跳转。

这对大型测试套件很直接。开发者可以继续抽取复用函数,不必为了定位失败而牺牲测试结构。失败报告会保留触发路径,排查时能从测试入口走到实际 assertion。

核心启发

  • 为数据校验写专用断言:做什么:把重复的 isValid(data) 检查封装成测试 helper。为什么值得做:XCTIssue 能把失败描述、数据附件和调用位置放进同一次失败记录。怎么开始:按官方片段创建 var issue = XCTIssue(type: .assertionFailure, compactDescription: ...),添加 XCTAttachment(data:),再调用 record(_:)
  • 给失败自动附加诊断文本:做什么:在某个测试类里覆盖 record(_:),为每次失败补充上下文。为什么值得做:测试报告会带上额外附件,CI result bundle 更容易直接阅读。怎么开始:将传入的 issue 复制为 var,调用 issue.add(XCTAttachment(string: ...)),最后调用 super.record(issue)
  • 为少数测试类加失败日志:做什么:只观察一个测试类里的失败,不影响整个测试 bundle。为什么值得做:演讲指出 record(_:) 是失败经过的入口,适合局部观察和日志。怎么开始:覆盖 record(_:),在 shouldLog(issue) 条件下记录信息,然后继续调用 super.record(issue)
  • 迁移会抛错的 setup 和 teardown:做什么:把需要抛错的准备和清理逻辑移到 setUpWithError()tearDownWithError()。为什么值得做:Xcode 12 会为抛出的错误报告源码位置,减少手写错误处理样板。怎么开始:保留旧方法处理继承需求,新测试优先使用带 Error 的生命周期方法。
  • 让共享测试工具继续复用:做什么:把重复 assertion 留在 helper 中,同时依赖 Xcode 12 的失败调用栈定位路径。为什么值得做:演讲展示了灰色入口标记和红色实际失败标记,能同时说明触发处和失败处。怎么开始:运行失败测试,在 Issue Navigator 或测试报告中沿 call stack 检查 frame。

关联 Session

评论

GitHub Issues · utterances