swift-testing by dpearson2699/swift-ios-skills
npx skills add https://github.com/dpearson2699/swift-ios-skills --skill swift-testingSwift Testing 是适用于 Swift 的现代测试框架(Xcode 16+,Swift 6+)。在所有新的单元测试中,应优先使用它而不是 XCTest。仅将 XCTest 用于 UI 测试、性能基准测试和快照测试。
import Testing
@Test("User can update their display name")
func updateDisplayName() {
var user = User(name: "Alice")
user.name = "Bob"
#expect(user.name == "Bob")
}
@Test("Validates email format") // 显示名称
@Test(.tags(.validation, .email)) // 标签
@Test(.disabled("Server migration in progress")) // 禁用
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // 条件启用
@Test(.bug("https://github.com/org/repo/issues/42")) // Bug 引用
@Test(.timeLimit(.minutes(1))) // 时间限制
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // 组合使用
广告位招租
在这里展示您的产品或服务
触达数万 AI 开发者,精准高效
// #expect 记录失败但继续执行
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")
// #expect 配合错误类型检查
#expect(throws: ValidationError.self) {
try validate(email: "not-an-email")
}
// #expect 配合特定错误值检查
#expect {
try validate(email: "")
} throws: { error in
guard let err = error as? ValidationError else { return false }
return err == .empty
}
// #require 记录失败并停止测试(类似于 XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")
// #require 用于可选值 —— 解包或失败
let first = try #require(items.first)
#expect(first.isValid)
规则:当后续断言依赖于该值时,使用 #require。对于独立的检查,使用 #expect。
关于套件组织、确认模式和已知问题处理,请参阅 references/testing-patterns.md。
标记预期失败,使其不会导致测试失败:
withKnownIssue("Propane tank is empty") {
#expect(truck.grill.isHeating)
}
// 间歇性/不稳定的失败
withKnownIssue(isIntermittent: true) {
#expect(service.isReachable)
}
// 条件已知问题
withKnownIssue {
#expect(foodTruck.grill.isHeating)
} when: {
!hasPropane
}
如果未记录任何已知问题,Swift Testing 会记录一个单独的问题,通知您问题可能已解决。
完整的示例请参阅 references/testing-patterns.md:
将任何 Sendable 和 Collection 传递给 arguments:。每个元素都作为独立的测试用例运行。
// 基于枚举:为每个枚举值运行一个用例
enum Environment: String, CaseIterable, Sendable {
case development, staging, production
}
@Test("Base URL is valid for all environments", arguments: Environment.allCases)
func baseURLIsValid(env: Environment) throws {
let url = try #require(URL(string: Config.baseURL(for: env)))
#expect(url.scheme == "https")
}
@Test("Fibonacci is positive for small inputs", arguments: 1...20)
func fibonacciPositive(n: Int) {
#expect(fibonacci(n) > 0)
}
两个参数集合会产生笛卡尔积(所有组合):
@Test(arguments: ["light", "dark"], ["iPhone", "iPad"])
func snapshotTest(colorScheme: String, device: String) {
// 运行 4 种组合:light+iPhone, light+iPad, dark+iPhone, dark+iPad
let config = SnapshotConfig(colorScheme: colorScheme, device: device)
#expect(config.isValid)
}
使用 zip 进行 1:1 配对(避免笛卡尔积):
@Test(arguments: zip(
[200, 201, 204],
["OK", "Created", "No Content"]
))
func httpStatusDescription(code: Int, expected: String) {
#expect(HTTPStatus(code).description == expected)
}
创建符合 CustomTestArgumentProviding 协议的类型或使用计算的静态属性:
struct APIEndpoint: Sendable {
let path: String
let expectedStatus: Int
static let testCases: [APIEndpoint] = [
.init(path: "/users", expectedStatus: 200),
.init(path: "/missing", expectedStatus: 404),
]
}
@Test("API returns expected status", arguments: APIEndpoint.testCases)
func apiStatus(endpoint: APIEndpoint) async throws {
let response = try await client.get(endpoint.path)
#expect(response.statusCode == endpoint.expectedStatus)
}
将标签声明为 Tag 上的静态成员:
extension Tag {
@Tag static var networking: Self
@Tag static var database: Self
@Tag static var slow: Self
@Tag static var critical: Self
@Tag static var smoke: Self
}
从 Xcode Test Plans 或命令行运行带标签的测试:
# 仅运行标记为 .networking 的测试
swift test --filter tag:networking
# 排除慢速测试
swift test --skip tag:slow
在 Xcode 中,配置 Test Plans 以在不同的 CI 配置中包含/排除标签(冒烟测试 vs 完整套件)。
@Suite("Shopping Cart Operations")
struct ShoppingCartTests {
let cart: ShoppingCart
// init 充当 setUp —— 在套件中的每个测试之前运行
init() {
cart = ShoppingCart()
cart.add(Product(name: "Widget", price: 9.99))
}
@Test func itemCount() {
#expect(cart.items.count == 1)
}
@Test func totalPrice() {
#expect(cart.total == 9.99)
}
}
使用 init 进行设置,使用 deinit 进行清理。对于异步清理,使用 TestScoping 特性:
@Suite(.tags(.database))
struct DatabaseTests {
let db: TestDatabase
init() async throws {
db = try await TestDatabase.createTemporary()
}
// deinit 适用于同步清理(结构体套件仅使用 init)
// 对于异步清理,请改用 TestScoping 特性
@Test func insertRecord() async throws {
try await db.insert(Record(id: 1, name: "Test"))
let count = try await db.count()
#expect(count == 1)
}
}
@Test 函数可以直接是 async 和 throws:
@Test func fetchUserProfile() async throws {
let service = UserService(client: MockHTTPClient())
let user = try await service.fetchProfile(id: 42)
#expect(user.name == "Alice")
}
使用 await 访问 actor 隔离的状态:
actor Counter {
private(set) var value = 0
func increment() { value += 1 }
}
@Test func counterIncrements() async {
let counter = Counter()
await counter.increment()
await counter.increment()
let value = await counter.value
#expect(value == 2)
}
使用 .timeLimit 防止测试无限期挂起:
@Test(.timeLimit(.seconds(5)))
func networkCallCompletes() async throws {
let result = try await api.fetchData()
#expect(result.isEmpty == false)
}
如果测试超过时间限制,它会立即失败并显示清晰的超时诊断信息。
confirmation 替代了 XCTestExpectation / fulfill() / waitForExpectations。它验证事件是否按预期次数发生:
@Test func notificationPosted() async throws {
// 期望闭包恰好调用 confirm() 一次
try await confirmation("UserDidLogin posted") { confirm in
let center = NotificationCenter.default
let observer = center.addObserver(
forName: .userDidLogin, object: nil, queue: .main
) { _ in
confirm()
}
await loginService.login(user: "test", password: "pass")
center.removeObserver(observer)
}
}
// 期望多次确认
@Test func batchProcessing() async throws {
try await confirmation("Items processed", expectedCount: 3) { confirm in
processor.onItemComplete = { _ in confirm() }
await processor.process(items: [a, b, c])
}
}
// 仅在 CI 上启用
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
func integrationTest() async throws { ... }
// 禁用并给出原因
@Test(.disabled("Blocked by #123 -- server migration"))
func brokenEndpoint() async throws { ... }
// Bug 引用 —— 将测试链接到问题跟踪器
@Test(.bug("https://github.com/org/repo/issues/42", "Intermittent timeout"))
func flakyNetworkTest() async throws { ... }
@Test(.timeLimit(.minutes(2)))
func longRunningImport() async throws {
try await importer.importLargeDataset()
}
// 对整个套件应用时间限制
@Suite(.timeLimit(.seconds(30)))
struct FastTests {
@Test func quick1() { ... }
@Test func quick2() { ... }
}
为常见的测试配置创建可重用的特性:
struct DatabaseTrait: TestTrait, SuiteTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
let db = try await TestDatabase.setUp()
defer { Task { await db.tearDown() } }
try await function()
}
}
extension Trait where Self == DatabaseTrait {
static var database: Self { .init() }
}
// 用法:任何带有 .database 特性的测试都会获得一个新的数据库
@Test(.database)
func insertUser() async throws { ... }
confirmation,而不是 sleep 调用。@Suite 中的 init() 设置自己的状态。sleep。 使用 confirmation、时钟注入或 withKnownIssue。Task 取消,请验证其是否干净地取消。@MainActor 标注依赖于主 Actor 的测试代码。将诊断数据附加到测试结果中,以便调试失败。完整示例请参阅 references/testing-patterns.md。
@Test func generateReport() async throws {
let report = try generateReport()
Attachment(report.data, named: "report.json").record()
#expect(report.isValid)
}
测试调用 exit()、fatalError() 或 preconditionFailure() 的代码。详情请参阅 references/testing-patterns.md。
@Test func invalidInputCausesExit() async {
await #expect(processExitsWith: .failure) {
processInvalidInput() // 调用 fatalError()
}
}
@Test,#expect),而不是 XCTest 断言fetchUserReturnsNilOnNetworkError 而不是 testFetchUser)confirmation(),而不是 Task.sleep.critical,.slow)references/testing-patterns.md每周安装量
390
代码仓库
GitHub Stars
269
首次出现
Mar 3, 2026
安全审计
安装于
codex387
cursor383
gemini-cli382
amp382
cline382
github-copilot382
Swift Testing is the modern testing framework for Swift (Xcode 16+, Swift 6+). Prefer it over XCTest for all new unit tests. Use XCTest only for UI tests, performance benchmarks, and snapshot tests.
import Testing
@Test("User can update their display name")
func updateDisplayName() {
var user = User(name: "Alice")
user.name = "Bob"
#expect(user.name == "Bob")
}
@Test("Validates email format") // display name
@Test(.tags(.validation, .email)) // tags
@Test(.disabled("Server migration in progress")) // disabled
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil)) // conditional
@Test(.bug("https://github.com/org/repo/issues/42")) // bug reference
@Test(.timeLimit(.minutes(1))) // time limit
@Test("Timeout handling", .tags(.networking), .timeLimit(.seconds(30))) // combined
// #expect records failure but continues execution
#expect(result == 42)
#expect(name.isEmpty == false)
#expect(items.count > 0, "Items should not be empty")
// #expect with error type checking
#expect(throws: ValidationError.self) {
try validate(email: "not-an-email")
}
// #expect with specific error value
#expect {
try validate(email: "")
} throws: { error in
guard let err = error as? ValidationError else { return false }
return err == .empty
}
// #require records failure AND stops test (like XCTUnwrap)
let user = try #require(await fetchUser(id: 1))
#expect(user.name == "Alice")
// #require for optionals -- unwraps or fails
let first = try #require(items.first)
#expect(first.isValid)
Rule: Use#require when subsequent assertions depend on the value. Use #expect for independent checks.
See references/testing-patterns.md for suite organization, confirmation patterns, and known-issue handling.
Mark expected failures so they do not cause test failure:
withKnownIssue("Propane tank is empty") {
#expect(truck.grill.isHeating)
}
// Intermittent / flaky failures
withKnownIssue(isIntermittent: true) {
#expect(service.isReachable)
}
// Conditional known issue
withKnownIssue {
#expect(foodTruck.grill.isHeating)
} when: {
!hasPropane
}
If no known issues are recorded, Swift Testing records a distinct issue notifying you the problem may be resolved.
See references/testing-patterns.md for complete examples of:
Pass any Sendable & Collection to arguments:. Each element runs as an independent test case.
// Enum-based: runs one case per enum value
enum Environment: String, CaseIterable, Sendable {
case development, staging, production
}
@Test("Base URL is valid for all environments", arguments: Environment.allCases)
func baseURLIsValid(env: Environment) throws {
let url = try #require(URL(string: Config.baseURL(for: env)))
#expect(url.scheme == "https")
}
@Test("Fibonacci is positive for small inputs", arguments: 1...20)
func fibonacciPositive(n: Int) {
#expect(fibonacci(n) > 0)
}
Two argument collections produce a cartesian product (every combination):
@Test(arguments: ["light", "dark"], ["iPhone", "iPad"])
func snapshotTest(colorScheme: String, device: String) {
// Runs 4 combinations: light+iPhone, light+iPad, dark+iPhone, dark+iPad
let config = SnapshotConfig(colorScheme: colorScheme, device: device)
#expect(config.isValid)
}
Use zip for 1:1 pairing (avoids cartesian product):
@Test(arguments: zip(
[200, 201, 204],
["OK", "Created", "No Content"]
))
func httpStatusDescription(code: Int, expected: String) {
#expect(HTTPStatus(code).description == expected)
}
Create a CustomTestArgumentProviding conformance or use computed static properties:
struct APIEndpoint: Sendable {
let path: String
let expectedStatus: Int
static let testCases: [APIEndpoint] = [
.init(path: "/users", expectedStatus: 200),
.init(path: "/missing", expectedStatus: 404),
]
}
@Test("API returns expected status", arguments: APIEndpoint.testCases)
func apiStatus(endpoint: APIEndpoint) async throws {
let response = try await client.get(endpoint.path)
#expect(response.statusCode == endpoint.expectedStatus)
}
Declare tags as static members on Tag:
extension Tag {
@Tag static var networking: Self
@Tag static var database: Self
@Tag static var slow: Self
@Tag static var critical: Self
@Tag static var smoke: Self
}
Run tagged tests from Xcode Test Plans or the command line:
# Run only tests tagged .networking
swift test --filter tag:networking
# Exclude slow tests
swift test --skip tag:slow
In Xcode, configure Test Plans to include/exclude tags for different CI configurations (smoke tests vs full suite).
@Suite("Shopping Cart Operations")
struct ShoppingCartTests {
let cart: ShoppingCart
// init acts as setUp -- runs before each test in the suite
init() {
cart = ShoppingCart()
cart.add(Product(name: "Widget", price: 9.99))
}
@Test func itemCount() {
#expect(cart.items.count == 1)
}
@Test func totalPrice() {
#expect(cart.total == 9.99)
}
}
Use init for setup and deinit for teardown. For async cleanup, use a TestScoping trait:
@Suite(.tags(.database))
struct DatabaseTests {
let db: TestDatabase
init() async throws {
db = try await TestDatabase.createTemporary()
}
// deinit works for synchronous cleanup (struct suites only use init)
// For async teardown, use TestScoping trait instead
@Test func insertRecord() async throws {
try await db.insert(Record(id: 1, name: "Test"))
let count = try await db.count()
#expect(count == 1)
}
}
@Test functions can be async and throws directly:
@Test func fetchUserProfile() async throws {
let service = UserService(client: MockHTTPClient())
let user = try await service.fetchProfile(id: 42)
#expect(user.name == "Alice")
}
Access actor-isolated state with await:
actor Counter {
private(set) var value = 0
func increment() { value += 1 }
}
@Test func counterIncrements() async {
let counter = Counter()
await counter.increment()
await counter.increment()
let value = await counter.value
#expect(value == 2)
}
Use .timeLimit to prevent tests from hanging indefinitely:
@Test(.timeLimit(.seconds(5)))
func networkCallCompletes() async throws {
let result = try await api.fetchData()
#expect(result.isEmpty == false)
}
If the test exceeds the time limit, it fails immediately with a clear timeout diagnostic.
confirmation replaces XCTestExpectation / fulfill() / waitForExpectations. It verifies that an event occurs the expected number of times:
@Test func notificationPosted() async throws {
// Expects the closure to call confirm() exactly once
try await confirmation("UserDidLogin posted") { confirm in
let center = NotificationCenter.default
let observer = center.addObserver(
forName: .userDidLogin, object: nil, queue: .main
) { _ in
confirm()
}
await loginService.login(user: "test", password: "pass")
center.removeObserver(observer)
}
}
// Expect multiple confirmations
@Test func batchProcessing() async throws {
try await confirmation("Items processed", expectedCount: 3) { confirm in
processor.onItemComplete = { _ in confirm() }
await processor.process(items: [a, b, c])
}
}
// Enable only on CI
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
func integrationTest() async throws { ... }
// Disable with a reason
@Test(.disabled("Blocked by #123 -- server migration"))
func brokenEndpoint() async throws { ... }
// Bug reference -- links test to an issue tracker
@Test(.bug("https://github.com/org/repo/issues/42", "Intermittent timeout"))
func flakyNetworkTest() async throws { ... }
@Test(.timeLimit(.minutes(2)))
func longRunningImport() async throws {
try await importer.importLargeDataset()
}
// Apply time limit to entire suite
@Suite(.timeLimit(.seconds(30)))
struct FastTests {
@Test func quick1() { ... }
@Test func quick2() { ... }
}
Create reusable traits for common test configurations:
struct DatabaseTrait: TestTrait, SuiteTrait, TestScoping {
func provideScope(
for test: Test,
testCase: Test.Case?,
performing function: @Sendable () async throws -> Void
) async throws {
let db = try await TestDatabase.setUp()
defer { Task { await db.tearDown() } }
try await function()
}
}
extension Trait where Self == DatabaseTrait {
static var database: Self { .init() }
}
// Usage: any test with .database trait gets a fresh database
@Test(.database)
func insertUser() async throws { ... }
confirmation with expected counts, not sleep calls.init() in @Suite.sleep in tests. Use confirmation, clock injection, or withKnownIssue.Task cancellation, verify it cancels cleanly.Attach diagnostic data to test results for debugging failures. See references/testing-patterns.md for full examples.
@Test func generateReport() async throws {
let report = try generateReport()
Attachment(report.data, named: "report.json").record()
#expect(report.isValid)
}
Test code that calls exit(), fatalError(), or preconditionFailure(). See references/testing-patterns.md for details.
@Test func invalidInputCausesExit() async {
await #expect(processExitsWith: .failure) {
processInvalidInput() // calls fatalError()
}
}
@Test, #expect), not XCTest assertionsfetchUserReturnsNilOnNetworkError not testFetchUser)confirmation(), not Task.sleep.critical, .slow)references/testing-patterns.mdWeekly Installs
390
Repository
GitHub Stars
269
First Seen
Mar 3, 2026
Security Audits
Gen Agent Trust HubPassSocketPassSnykPass
Installed on
codex387
cursor383
gemini-cli382
amp382
cline382
github-copilot382
Spring Boot工程师技能指南:微服务架构、安全加固与云原生开发实战
2,800 周安装
@MainActor.