宏测试

CI Slack

用于 Swift 宏的魔法般的测试工具。

An animated demonstration of macro tests being inlined.

了解更多

此库旨在支持为 Point-Free 制作的库和剧集,Point-Free 是一个由 Brandon WilliamsStephen Celis 主持的 Swift 编程语言探索视频系列。

您可以在此处观看所有剧集。

video poster image

动机

此库附带一个用于测试宏的工具,它比 SwiftSyntax 自带的默认工具更强大且更符合人体工程学。要使用该工具,只需指定您要扩展的宏以及使用该宏的 Swift 源代码字符串即可。

例如,要测试 SPM 宏模板附带的 #stringify 宏,只需编写以下内容

import MacroTesting
import XCTest

class StringifyTests: XCTestCase {
  func testStringify() {
    assertMacro(["stringify": StringifyMacro.self]) {
      """
      #stringify(a + b)
      """
    }
  }
}

当您运行此测试时,该库将自动扩展源代码字符串中的宏,并将扩展写入测试文件

func testStringify() {
  assertMacro(["stringify": StringifyMacro.self]) {
    """
    #stringify(a + b)
    """
  } expansion: {
    """
    (a + b, "a + b")
    """
  }
}

这就是全部。

如果将来宏的输出发生更改,例如向元组的参数添加标签,则再次运行测试将产生格式良好的消息

❌ 失败 - 实际输出 (+) 与预期输出 (−) 不同。差异:…

- (a + b, "a + b")
+ (result: a + b, code: "a + b")

您甚至可以通过向 assertMacro 提供 record 参数,让库自动将宏扩展重新记录到您的测试文件中

assertMacro(["stringify": StringifyMacro.self], record: .all) {
  """
  #stringify(a + b)
  """
} expansion: {
  """
  (a + b, "a + b")
  """
}

现在,当您再次运行测试时,最新的扩展宏将写入 expansion 尾随闭包。

如果您要为宏编写许多测试,则可以使用 XCTest 的 invokeTest 方法将每个测试与宏测试配置包装起来,从而避免在每个断言中重复指定宏的繁琐工作

class StringifyMacroTests: XCTestCase {
  override func invokeTest() {
    withMacroTesting(
      macros: ["stringify": StringifyMacro.self]
    ) {
      super.invokeTest()
    }
  }

  func testStringify() {
    assertMacro {  // 👈 No need to specify the macros being tested
      """
      #stringify(a + b)
      """
    } expansion: {
      """
      (a + b, "a + b")
      """
    }
  }

  // ...
}

您可以将 record 参数传递给 withMacroTesting,以重新记录测试用例(或套件,如果您使用自己的自定义基本测试用例类)中的每个断言

override func invokeTest() {
  withMacroTesting(
    record: .all
  ) {
    super.invokeTest()
  }
}

宏测试还可以测试诊断信息,例如警告、错误、注释和修复建议。当宏扩展发出诊断信息时,它将以内联方式呈现在测试中。例如,向异步函数添加完成处理程序函数的宏应用于非异步函数时,可能会发出错误和修复建议。生成的宏测试将完全捕获此信息,包括诊断信息的发出位置、修复建议的应用方式以及最终宏的扩展方式

func testNonAsyncFunctionDiagnostic() {
  assertMacro {
    """
    @AddCompletionHandler
    func f(a: Int, for b: String) -> String {
      return b
    }
    """
  } diagnostics: {
    """
    @AddCompletionHandler
    func f(a: Int, for b: String) -> String {
    ┬───
    ╰─ 🛑 can only add a completion-handler variant to an 'async' function
       ✏️ add 'async'
      return b
    }
    """
  } fixes: {
    """
    @AddCompletionHandler
    func f(a: Int, for b: String) async -> String {
      return b
    }
    """
  } expansion: {
    """
    func f(a: Int, for b: String) async -> String {
      return b
    }

    func f(a: Int, for b: String, completionHandler: @escaping (String) -> Void) {
      Task {
        completionHandler(await f(a: a, for: b, value))
      }
    }
    """
  }
}

与 Swift Testing 集成

如果您正在使用 Swift 的内置 Testing 框架,则此库也支持它。您可以不只依赖 XCTest,还可以使用 swift-testing 提供的 Trait 系统配置您的测试。例如

import Testing
import MacroTesting

@Suite(
  .macros(
    record: .missing // Record only missing snapshots
    macros: ["stringify": StringifyMacro.self],
  )
)
struct StringifyMacroSwiftTestingTests {
  @Test
  func testStringify() {
    assertMacro {
      """
      #stringify(a + b)
      """
    } expansion: {
      """
      (a + b, "a + b")
      """
    }
  }
}

此外,macros 中的 record 参数可用于控制套件中所有测试的记录行为。此值也可以使用 SNAPSHOT_TESTING_RECORD 环境变量进行配置,以根据您的 CI 或本地环境动态调整记录行为。

文档

此库的最新文档可在此处获取。

许可证

此库在 MIT 许可证下发布。有关详细信息,请参阅 LICENSE