Re:source
WWDC 2024 - 了解 Swift Testing

WWDC 2024 - 了解 Swift Testing

WWDC 2024 - 了解 Swift Testing

Building Block

import Testing
@testable import DestinationVideo

@Test("Check video metadata") func videoMetaData() {
    let video = Video(fileName: "By the Lake.mov")
    let expectedMetadata = Metadata(duration: .seconds(90))
    #expect(video.metadata == expectedMetadata)
}

@Test 宏用来标记这个函数是用来测试的;

默认访问修饰符是 internal,范围为整个 module,@testable 相当于可以解除这个禁制,让测试访问到。

#expect 宏用来测试条件是否为真

#require 宏用来断言,如果条件为假,会直接抛出异常,立即停止测试

try #require(session.isValid)

session.invalidate() // not executed

也可以用来断言可选类型,当空时退出测试

let method = try #require(paymentMethods.first)

#expect(method.isDefault) // not executed

Traits

  • 展示测试描述

  • 定制测试用例什么时候跑,是否跑

  • 修改测试用例执行的行为

一些内置的 Traits,更多详见文档

Test Suite

可以将关联的测试函数放在一起,使用 @Suite 宏,可以有实例变量,也可以使用 init 和 deinit 来添加初始化与销毁的一些逻辑

struct VideoTests {

    @Test("Check video metadata") func videoMetadata() {
        let video = Video(fileName: "By the Lake.mov")
        let expectedMetadata = Metadata(duration: .seconds(90))
        #expect(video.metadata == expectedMetadata)
    }

    @Test func rating() async throws {
        let video = Video(fileName: "By the Lake.mov")
        #expect(video.contentRating == "G")
    }

}

@Suite 实例为每一个 @Test 方法都初始化一个新的防止状态共享。所以上面的代码可以改写成:

struct VideoTests {
    let video = Video(fileName: "By the Lake.mov")

    @Test("Check video metadata") func videoMetadata() {
        let expectedMetadata = Metadata(duration: .seconds(90))
        #expect(video.metadata == expectedMetadata)
    }

    @Test func rating() async throws {
        #expect(video.contentRating == "G")
    }

}

Common workflows

运行时条件 Runtime conditions,用来控制测试什么时候跑。

.enabled(if: ...) ,当条件为 false 时会跳过测试

@Test(.enabled(if: AppFeatures.isCommentingEnabled)) // Test 'videoCommenting()' skipped
func videoCommenting() {
    // ...
}

.disabled(...) ,可以描述为什么这个测试被禁用

@Test(.disabled("Due to a known crash"))
func example() {
    // ...
}

.bug(...),可以用来关联一个 issue

@Test(.disabled("Due to a known crash"), .bug("example.org/bugs/1234", "Program crashes at <symbol>"))
func example() {
    // ...
}

可以基于操作系统版本可用性来作为条件,使用 .available(...) 而不是 #available(...)

// Not recommend
@Test func hasRuntimeVersionCheck() {
    guard #available(macOS 15, *) else { return }
}

@Test
@available(macOS 15, *)
func usesNewAPIs() {
    // ...
}

使用 .tags(...) 来给测试添加标签,用来分组,方便在 Xcode 中根据标签分组查看

带参数的测试 parameterized testing,使用 arguments

// Before
struct VideoContinentsTests {

    @Test func mentionsFor_A_Beach() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "A Beach"))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }

    @Test func mentionsFor_By_the_Lake() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "By the Lake"))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }

    @Test func mentionsFor_Camping_in_the_Woods() async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: "Camping in the Woods"))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }

    // ...and more, similar test functions
}

// After
struct VideoContinentsTests {

    @Test("Number of mentioned continents", arguments: [
        "A Beach",
        "By the Lake",
        "Camping in the Woods",
        "The Rolling Hills",
        "Ocean Breeze",
        "Patagonia Lake",
        "Scotland Coast",
        "China Paddy Field",
    ])
    func mentionedContinentCounts(videoName: String) async throws {
        let videoLibrary = try await VideoLibrary()
        let video = try #require(await videoLibrary.video(named: videoName))
        #expect(!video.mentionedContinents.isEmpty)
        #expect(video.mentionedContinents.count <= 3)
    }

}

Swift Testing and XCTest

XCTest 与 Swift Testing 的对比:

更多信息可以查看Migrating a test from XCTest

Open Source

全平台支持,SPM 命令行、Xcode、VSCode Swift Extension

Command line: swift test

其他信息