Xcode 16 Swift Testing进行单元测试

866 阅读4分钟

本文章介绍 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添加到 SwiftClass/Sturct/Enum 类型,包含@Test函数或套件的类型将被隐式注释,并鼓励使用结构体来隔离状态。需注意将 @Suite添加到 Enum 类型意义是不大的,因为在 Swift Testing 中,测试套件只能包含无参数的 init(),因此不能在枚举中直接定义测试用例。