本文章介绍 Xcode 16 WWDC 2024 新增功能 Swift Testing,并默认你已经对 XCTest 有了简单的了解
一、在 Xcode 项目中集成 Swift Testing
Swift Testing 已被无缝集成到 Xcode 16 中,成为官方首推的测试框架。在创建新项目时,你可以轻松选择 Swift Testing 作为默认测试框架,如下图所示:
二、初步认识
先来看一个非常简单的 test。
import Testing
@testable import SwiftTestingDemo
struct SwiftTestingDemoTests {
@Test func example() async throws {
// Write your test here and use APIs like `#expect(...)` to check expected conditions.
#expect(2 > 1)
}
}
发现语法上确实和原来的 XCTest 有了很大的不同。具体有哪些变化我先来做个简单的罗列:
1.取消了继承 XCTestCase,而且声明的类型是个结构体。
2.方法名也不再强制以 test 作为前缀。取而代之的是通过 @Test 来标识为需要测试的 function。
3.判断不再是通过 XCTAssertXXX,改为了 #expect。
4.setUpWithError()、tearDownWithError() 方法没有了。
详细看下 WWDC Swift Testing 视频介绍。
三、详细介绍
将会分为如下几个部分来进行分析
1、 Functions
测试方法就是 Swift 方法,只是多了 @Test 修饰。可以是全局方法,也可以被包装在 Struct/Class 中。
@Test func example() {
#expect(2 > 1)
}
@Test func example() async throws {
#expect(2 > 1)
}
2、 Exceptions
#expect 宏非常灵活,可以用来执行预期操作。也支持表达式与运算符
#expect(1 == 2)
#expect(user.name == "Alice")
#expect(!array.isEmpty)
#expect(numbers.contains(1))
struct TestEntity {
enum CalculationError: Swift.Error, Equatable {
case divisionByZero
}
func division(_ a: Int, _ b: Int) throws -> Int {
guard b != 0 else {
throw CalculationError.divisionByZero
}
return a / b
}
}
@Test func testThrowErrors() throws {
let sut = TestEntity()
#expect(throws: (any Error).self) {
try sut.division(1, 0)
}
#expect(throws: TestEntity.CalculationError.divisionByZero) {
try sut.division(1, 0)
}
#expect {
try sut.division(1, 0)
} throws: { error in
guard let error = error as? TestEntity.CalculationError,
case .divisionByZero = error else {
return false
}
return true
}
}
3、 #require:
与 #expect 类似,但需要结合 try 关键字,并且会在表达式失败时抛出错误。如果你期望可以在预期操作失败时提前结束测试,那么这个宏将会是你需要的。
@Test func testIsValid() throws {
let isValid = true
let _ = try #require(isValid)// Test failed when `isValid == false`.
#expect(isValid == true)// Not excuted when `isValid == false`.
}
//也可以对可选值解包进行提前处理。当 optionValue 为 nil 时,则会测试失败并提前结束测试。
@Test func testOptionalValue() throws {
let optionValue: Int? = 0
let unwrapValue = try #require(optionValue)
#expect(unwrapValue != nil)
let array: [Int] = []
// Warning: Test failure when you open following line comment!!!
// let _ = try #require(array.first)
}
try #require(throws: (any Error).self) {
try sut.division(1, 0)
}
4 、Traits
特征描述部分是 Swift Testing 的一个很重要的点。
•可以添加关于测试的描述信息,如 @Test("Custom name")
•添加自定义标签,如 @Test(.tags(.critical))
•自定义是否运行测试,如 @Test(.enabled/.disabled)...
•还可以修改测试的运行方式,如 @Test(.timeLimit(.minutes(3)))、@Suite(.serialized)
Built-in traits
@Test("Custom name") Customize the display name of a test
@Test(.bug("example.com/issues/99999", "Title")) Reference an issue from a bug tracker
@Test(.tags(.critical)) Add a custom tag to a test
@Test(.enabled(if: Server.isOnline)) Specify a runtime condition for a test
@Test(.disabled("Currently broken")) Unconditionally disable a test
@Test(...) @available(macOS 15, *) Limit a test to certain OS versions
@Test(.timeLimit(.minutes(3))) Set a maximum time limit for a test
@Suite(.serialized) Run the tests in a suite one at a time, without parallelization
5、Custom Name
@Test("这里可以自定义测试方法名")
func renameTestFunction() {
var boolValue = false
#expect(!boolValue)
boolValue = true
#expect(boolValue)
}
◇ Test "这里可以自定义测试方法名" started.
✔ Test "这里可以自定义测试方法名" passed after 0.001 seconds.
✔ Test run with 1 test passed after 0.001 seconds.
6、Bug
可以引用相关 Issue 链接。
@Test(.bug("https://github.com/example/"))
func bugExample() throws {
// ...
}
7、Tag
extension Tag {
@Tag static var formatting: Self
@Tag static var networking: Self
}
@Test(.tags(.formatting))
func tagSampleTest1() {
let a = 2
#expect(a < 3)
}
@Test(.tags(.networking, .formatting))
func tagSampleTest2() throws {
let a = 2
try #require(a < 3)
}
8、 Enabled & Disabled
/// Modify this value to change test enable state.
let isTestEnabled: Bool = false
@Test(.enabled(if: isTestEnabled)) func testFuncEnabled() {
// ✘ Test testFuncEnabled() skipped.
}
@Test(.disabled(if: !isTestEnabled)) func testFuncDisabled() {
// ✘ Test testFuncDisabled() skipped.
}
✘ Test testFuncEnabled() skipped.
✘ Test testFuncDisabled() skipped.
可以直接通过 @Test(.disabled("Comment")) 来让测试方法强制跳过,当然也可以选择注释掉或删除。通过这种方式来处理仍可以触发编译,变相验证代码有效性。当然这只是增加了一种方式,采用何种方式,完全取决于你。
@Test(.disabled("Explain the reason for func skipping."))
func testFuncWillBeSkipped() {
let array: [Int] = []
#expect(array[0] == 0)
}
9、TimeLimit: 设置测试的最大运行时间,超时则视为测试失败。
@available(iOS 16.0, *)
@Test(.timeLimit(.minutes(1)))
func testTimeLimit() async throws {
let sleepTime = 1// Modify it to greate than 60 will be failed.
try await Task.sleep(for: .seconds(sleepTime))
}
10、Serialized
Swift Testing 默认并行运行测试函数,这将加快测试运行速度,并以随机顺序运行,以帮助识别测试之间的隐藏依赖关系。如果想按顺序进行测试,即可通过 @Suite(.serialized) 特性来实现。
@Suite(.serialized)
struct SerializedTests {
@Test func serializedSampleTest1() throws {
let a = 2
try #require(a < 3)
}
@Test func serializedSampleTest2() throws {
let a = 2
try #require(a < 3)
}
@Test func serializedSampleTest3() throws {
let a = 2
try #require(a < 3)
}
}
◇ Suite SerializedTests started.
◇ Test serializedSampleTest1() started.
✔ Test serializedSampleTest1() passed after 0.001 seconds.
◇ Test serializedSampleTest2() started.
✔ Test serializedSampleTest2() passed after 0.001 seconds.
◇ Test serializedSampleTest3() started.
✔ Test serializedSampleTest3() passed after 0.001 seconds.
✔ Suite SerializedTests passed after 0.001 seconds.
✔ Test run with 3 tests passed after 0.002 seconds.
Suites:可以将@Suite添加到 Swift 的 Class/Sturct/Enum 类型,包含@Test函数或套件的类型将被隐式注释,并鼓励使用结构体来隔离状态。需注意将 @Suite添加到 Enum 类型意义是不大的,因为在 Swift Testing 中,测试套件只能包含无参数的 init(),因此不能在枚举中直接定义测试用例。