スキル一覧に戻る
onmyway133

swift-testing

by onmyway133

My favorite Claude Code tools

3🍴 0📅 2026年1月12日
GitHubで見るManusで実行

SKILL.md


name: "Swift Testing" description: "Modern Swift Testing framework patterns and best practices"

Swift Testing

You are a Swift testing expert using the modern Swift Testing framework (not XCTest). Apply these patterns when writing tests.

Framework Basics

Import and Structure

import Testing
@testable import MyApp

struct UserTests {
    @Test func createsUserWithValidEmail() {
        let user = User(email: "test@example.com")
        #expect(user.isValid)
    }
}

Key Differences from XCTest

XCTestSwift Testing
class with XCTestCasestruct (preferred) or class
func testSomething()@Test func something()
XCTAssertEqual(a, b)#expect(a == b)
XCTAssertTrue(x)#expect(x)
XCTAssertNil(x)#expect(x == nil)
XCTAssertThrowsError#expect(throws:)
setUpWithError()init()
tearDown()deinit

Expectations

Basic Expectations

@Test func basicExpectations() {
    let value = 42

    // Boolean
    #expect(value > 0)
    #expect(value == 42)

    // Optionals
    let optional: String? = "hello"
    #expect(optional != nil)

    // Collections
    let array = [1, 2, 3]
    #expect(array.contains(2))
    #expect(array.count == 3)
}

Require (Unwrapping)

@Test func requireUnwrapping() throws {
    let optional: String? = "hello"

    // Unwrap or fail test
    let value = try #require(optional)
    #expect(value == "hello")

    // With custom message
    let user = try #require(fetchUser(), "User should exist")
    #expect(user.isActive)
}

Error Expectations

@Test func errorHandling() {
    // Expect any error
    #expect(throws: (any Error).self) {
        try riskyOperation()
    }

    // Expect specific error type
    #expect(throws: ValidationError.self) {
        try validate(email: "invalid")
    }

    // Expect specific error value
    #expect(throws: ValidationError.invalidEmail) {
        try validate(email: "")
    }
}

Test Organization

Suites (Grouping)

@Suite("User Authentication")
struct AuthenticationTests {

    @Suite("Login")
    struct LoginTests {
        @Test func validCredentials() { }
        @Test func invalidPassword() { }
    }

    @Suite("Registration")
    struct RegistrationTests {
        @Test func newUser() { }
        @Test func duplicateEmail() { }
    }
}

Setup and Teardown

struct DatabaseTests {
    let database: Database

    // Setup in init
    init() async throws {
        database = try await Database.createTest()
    }

    @Test func insertsRecord() async throws {
        try await database.insert(record)
        #expect(await database.count == 1)
    }
}

Parameterized Tests

Basic Parameterization

@Test(arguments: ["hello", "world", "swift"])
func stringIsNotEmpty(value: String) {
    #expect(!value.isEmpty)
}

@Test(arguments: 1...10)
func positiveNumbers(n: Int) {
    #expect(n > 0)
}

Multiple Parameters

@Test(arguments: [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0)
])
func addition(a: Int, b: Int, expected: Int) {
    #expect(a + b == expected)
}

Zip vs Product

// Zip: pairs elements (3 tests)
@Test(arguments: zip([1, 2, 3], ["a", "b", "c"]))
func zipped(number: Int, letter: String) { }

// Product: all combinations (9 tests)
@Test(arguments: [1, 2, 3], ["a", "b", "c"])
func product(number: Int, letter: String) { }

Tags and Traits

Tagging Tests

extension Tag {
    @Tag static var slow: Self
    @Tag static var network: Self
    @Tag static var critical: Self
}

@Test(.tags(.slow, .network))
func networkRequest() async { }

@Test(.tags(.critical))
func paymentProcessing() { }

Display Names

@Test("User can update profile picture")
func updateProfilePicture() { }

@Suite("Payment Processing")
struct PaymentTests { }

Conditional Tests

// Disable with reason
@Test(.disabled("Waiting for API update"))
func upcomingFeature() { }

// Platform-specific
@Test(.enabled(if: ProcessInfo.processInfo.environment["CI"] != nil))
func ciOnly() { }

// Runtime conditions
@Test
func featureFlagTest() throws {
    try #require(FeatureFlags.newCheckout, "Feature not enabled")
    // Test new checkout...
}

Time Limits

@Test(.timeLimit(.minutes(1)))
func longRunningOperation() async { }

@Test(.timeLimit(.seconds(5)))
func quickOperation() { }

Async Testing

Async Tests

@Test func asyncFetch() async throws {
    let service = DataService()
    let result = try await service.fetchData()
    #expect(!result.isEmpty)
}

Actor Testing

@Test func actorState() async {
    let counter = Counter()
    await counter.increment()
    await counter.increment()

    let value = await counter.value
    #expect(value == 2)
}

Confirmation (Async Expectations)

@Test func notificationReceived() async {
    await confirmation { confirm in
        NotificationCenter.default.addObserver(
            forName: .userLoggedIn,
            object: nil,
            queue: nil
        ) { _ in
            confirm()
        }

        await loginUser()
    }
}

// Multiple confirmations
@Test func multipleEvents() async {
    await confirmation(expectedCount: 3) { confirm in
        eventHandler.onEvent = { confirm() }
        triggerThreeEvents()
    }
}

Best Practices

Naming

// Good: describes behavior
@Test func calculatesDiscountForPremiumUsers() { }
@Test func throwsErrorWhenEmailInvalid() { }
@Test func returnsEmptyArrayWhenNoResults() { }

// Avoid: vague or implementation-focused
@Test func testDiscount() { }  // What about discount?
@Test func testMethod() { }    // Which method?

Arrange-Act-Assert

@Test func appliesCouponCode() throws {
    // Arrange
    let cart = ShoppingCart()
    cart.addItem(Product(price: 100))
    let coupon = Coupon(code: "SAVE20", discount: 0.20)

    // Act
    try cart.apply(coupon)

    // Assert
    #expect(cart.total == 80)
    #expect(cart.appliedCoupons.contains(coupon))
}

One Concept Per Test

// Good: focused tests
@Test func addsItemToCart() {
    cart.add(item)
    #expect(cart.items.contains(item))
}

@Test func updatesCartTotal() {
    cart.add(item)
    #expect(cart.total == item.price)
}

// Avoid: testing multiple concepts
@Test func cartOperations() {
    cart.add(item)
    #expect(cart.items.contains(item))
    cart.remove(item)
    #expect(cart.items.isEmpty)
    cart.add(item)
    cart.add(item2)
    #expect(cart.total == item.price + item2.price)
}

Test Fixtures

struct OrderTests {
    // Shared fixtures
    let validProduct = Product(id: "1", name: "Widget", price: 9.99)
    let customer = Customer(id: "c1", tier: .standard)

    @Test func createsOrder() {
        let order = Order(customer: customer, items: [validProduct])
        #expect(order.isValid)
    }

    @Test func calculatesShipping() {
        let order = Order(customer: customer, items: [validProduct])
        #expect(order.shippingCost == 5.99)
    }
}

Migration from XCTest

Swift Testing was introduced at WWDC 2024 and ships with Xcode 16. XCTest is not deprecated - migrate incrementally at your own pace.

What Can Be Migrated

  • Unit tests with assertions
  • Async/await tests
  • Tests using setUp/tearDown

What Cannot Be Migrated

  • UI tests using XCUIApplication
  • Performance tests using XCTMetric
  • Objective-C tests

Continue using XCTest for these cases.

Critical Rule

Never mix frameworks within a single test. Don't call XCTAssert from Swift Testing or #expect from XCTest.

Side-by-Side

Swift Testing can coexist with XCTest in the same target:

import XCTest
import Testing

// Old XCTest - keep as-is
class LegacyTests: XCTestCase {
    func testOldWay() {
        XCTAssertTrue(true)
    }
}

// New Swift Testing
struct ModernTests {
    @Test func newWay() {
        #expect(true)
    }
}

Complete Migration Table

XCTestSwift Testing
class MyTests: XCTestCasestruct MyTests
func testSomething()@Test func something()
override func setUp()init()
override func tearDown()deinit
XCTAssert(x)#expect(x)
XCTAssertTrue(x)#expect(x)
XCTAssertFalse(x)#expect(!x)
XCTAssertNil(x)#expect(x == nil)
XCTAssertNotNil(x)#expect(x != nil)
XCTAssertEqual(a, b)#expect(a == b)
XCTAssertNotEqual(a, b)#expect(a != b)
XCTAssertGreaterThan(a, b)#expect(a > b)
XCTAssertLessThan(a, b)#expect(a < b)
XCTUnwrap(x)try #require(x)
XCTFail("message")Issue.record("message")
XCTAssertThrowsError#expect(throws:)
XCTAssertNoThrow#expect(throws: Never.self)
expectation + fulfill + waitawait confirmation

Migration Examples

Setup/Teardown:

// XCTest
class StoreTests: XCTestCase {
    var store: DataStore!

    override func setUpWithError() throws {
        store = try DataStore(inMemory: true)
    }

    override func tearDown() {
        store = nil
    }
}

// Swift Testing
struct StoreTests {
    let store: DataStore

    init() throws {
        store = try DataStore(inMemory: true)
    }
}

Assertions:

// XCTest
XCTAssertEqual(user.name, "Alice", "Name should be Alice")

// Swift Testing
#expect(user.name == "Alice", "Name should be Alice")

Unwrapping:

// XCTest
let user = try XCTUnwrap(fetchUser())

// Swift Testing
let user = try #require(fetchUser())

Error Testing:

// XCTest
XCTAssertThrowsError(try validate("")) { error in
    XCTAssertEqual(error as? ValidationError, .empty)
}

// Swift Testing
#expect(throws: ValidationError.empty) {
    try validate("")
}

Async Expectations:

// XCTest
let exp = expectation(description: "Callback received")
service.onComplete = { exp.fulfill() }
service.start()
wait(for: [exp], timeout: 5)

// Swift Testing
await confirmation { confirm in
    service.onComplete = { confirm() }
    service.start()
}

Consolidation Tips

Multiple similar tests → Parameterized test:

// XCTest: 3 separate test methods
func testParseValidEmail1() { ... }
func testParseValidEmail2() { ... }
func testParseValidEmail3() { ... }

// Swift Testing: 1 parameterized test
@Test(arguments: ["a@b.com", "test@example.org", "user@domain.co"])
func parsesValidEmail(_ email: String) {
    #expect(Email(email) != nil)
}

Single-method class → Global function:

// XCTest
class SingleTest: XCTestCase {
    func testOneThing() { ... }
}

// Swift Testing
@Test func oneThing() { ... }

Migration Strategy

  1. Start with new tests - write them in Swift Testing
  2. Migrate simple unit tests first
  3. Leave UI and performance tests in XCTest
  4. Consolidate similar tests into parameterized tests
  5. No rush - both frameworks work side-by-side indefinitely

Running Tests

Command Line

# Run all tests
swift test

# Run specific suite
swift test --filter UserTests

# Run with tags
swift test --filter "tag:critical"

# Parallel execution (default)
swift test --parallel

Xcode

  • Cmd+U - Run all tests
  • Cmd+Ctrl+U - Run test at cursor
  • Test Navigator shows results organized by suite

スコア

総合スコア

60/100

リポジトリの品質指標に基づく評価

SKILL.md

SKILL.mdファイルが含まれている

+20
LICENSE

ライセンスが設定されている

+10
説明文

100文字以上の説明がある

0/10
人気

GitHub Stars 100以上

0/15
最近の活動

3ヶ月以内に更新

+5
フォーク

10回以上フォークされている

0/5
Issue管理

オープンIssueが50未満

+5
言語

プログラミング言語が設定されている

0/5
タグ

1つ以上のタグが設定されている

+5

レビュー

💬

レビュー機能は近日公開予定です