BCTest

Swift 包 BCTest 提供了在 Swift 测试中断言异步条件的功能,其人体工程学设计与 XCTestExpectation 类似。

@Test @BCTest myTest() async throws {
  let myNeed = await needManager.need(expectedCount: 1)
  let myOtherNeed = await needManager.need(expectedCount: 1)
  Task.detached { 
    myNeed.satisfy()
    myOtherNeed.satisfy()
  }
  try await awaitSatisfaction(of: [myNeed, myOtherNeed], timeout: .infinity)
}

needManagerBCTestNeedManager 的一个实例。@BCTest 宏在 Swift 测试主体的顶部创建 needManager。使用 needManager 来创建 BCTestNeed 的实例。

使用全局函数 awaitSatisfaction(of:timeout:) 等待 BCTestNeed 的满足。

所有已创建的 need 都必须被等待 (awaited) 且被满足 (satisfied),Swift 测试才能通过。@BCTest 宏将 await needManager.assertNeedsSatisfied() 添加到 Swift 测试主体的末尾。

展开后的宏

@Test @BCTest myTest() async throws {
  let needManager = BCTestNeedManager() // *** ADDED BY MACRO ***
  let myNeed = await needManager.need(expectedCount: 1)
  let myOtherNeed = await needManager.need(expectedCount: 1)
  Task.detached { 
    myNeed.satisfy()
    myOtherNeed.satisfy()
  }
  try await awaitSatisfaction(of: [myNeed, myOtherNeed], timeout: .infinity)
  await needManager.assertNeedsSatisfied() // *** ADDED BY MACRO ***
}

一个测试 need 被配置为期望零次或多次 satisfy() 的调用。一个测试 need 被配置为在过度满足 (over satisfied) 时是否失败。

一个 BCTestNeed 只能由一个 BCTestNeedManager 创建。

对 need 满足的断言只能由创建它的 need 管理器执行。

有意不满足

如果一个 need 永远不会被满足,则将其配置为 expectedCount: 0。如果在 need 上调用 satisfy()(例如,在不应该执行的代码路径中),该 need 将被过度满足,并且测试将失败。

有意的断言失败

如果一个测试被设计为 need 管理器的断言应该失败,请包含 .withKnownIssue 特性以避免测试失败,该特性将 need 管理器的断言包装在 withKnownIssue {..} 中。

不变性

BCTest 提供了以下不变性

注释:在等待结束后调用 satisfy() 会增加 need 的满足计数,但不能算作满足。

脆弱性

宏展开要求 BCTestNeedManager 的方法 initassertNeedsSatisfied() 是公共的,但是你不应该创建另一个 need 管理器,因为宏不能确保 need 管理器断言满足并等待它创建的 need。

如果一个测试辅助函数创建了 need,请将宏提供的 needManager 传递给它。

作为 ConfirmationCheckedContinuation 的替代方案

建议使用 ConfirmationCheckedContinuation 在 Swift 测试中测试异步事件,但它们的人体工程学友好性不如 XCTest 的 XCTestExpectation

Confirmation 要求确认发生在它的 body 参数退出之前。await confirmation() { c in Task { c() } } 会失败,而 await confirmation() { c in await Task { c() }.value } 会通过。

CheckedContinuation 不需要在其 body 参数退出之前 resume()

await withCheckedContinuation { c in 
  Task {
    let cookies = await chimChim.threwCookies()
    #expect(cookies.count == 1)
    c.resume()
  }
}

但是 CheckedContinuation 只能 resume() 一次,因此无法在内部跟踪将要发生多次的事件。

此外,CheckedContinuation 需要小心,确保 resume() 在所有 @expect/#require 语句*之后*被调用,否则 Swift 测试上下文可能会错过它们。请参阅 Tests/AlternativeTests 中的 CheckedContinuationShouldFailCheckedContinuationWithKnownIssue 测试。

最后,ConfirmationCheckedContinuation 与一个闭包以及闭包内发生的确认/延续具有 1:1 的关系。一个确保多个异步事件发生的测试可能需要嵌套这些结构的实例。

许可证

此包在 Unlicensed 许可证下发布。