プログラミング

【Swift Testing入門】XCTestからの移行ガイド|@Testマクロと#expectで始める新しいテストの書き方

記事内に商品プロモーションを含む場合があります

iOS開発でユニットテストを書いている方なら、XCTestフレームワークを使った経験があるかと思います。WWDC23で発表されたSwift Testingは、Swiftの言語機能をフル活用した新しいテストフレームワークで、XCTestと比べてよりシンプルで表現力豊かなテストが書けるようになっています。

この記事では、Swift Testingの基本的な使い方から、XCTestとの違い、そして既存プロジェクトからの移行方法までを実践的に解説していきます。すでにXCTestでテストを書いたことがある方を対象にしているため、テストの基本概念については説明を省略します。

Swift TestingとXCTestの比較イメージ

Swift Testingとは

Swift TestingはAppleが開発した新しいテストフレームワークで、iOS 17.0以降、macOS 14.0以降で利用可能です。Xcode 16からは新規プロジェクト作成時にSwift Testingがデフォルトで選択されるようになっています。

主な特徴として、Swiftのマクロを活用した簡潔な記法、パラメタライズドテスト(引数付きテスト)のネイティブサポート、そしてXCTestとの共存が可能な点が挙げられます。

@Testマクロの基本

Swift Testingでテストを書く際の基本は、関数に@Testマクロを付けることです。XCTestのようにtestプレフィックスを付ける必要はありません。

import Testing

@Test func additionWorks() {
    let result = 2 + 3
    #expect(result == 5)
}

これだけでテストとして認識されます。関数名は自由に付けられるため、テストの意図がより明確に伝わる名前を付けやすくなっています。

また、@Testマクロには表示名を指定することもできます。

@Test("2つの数値を足すと正しい結果が返る")
func additionWorks() {
    let result = 2 + 3
    #expect(result == 5)
}

#expectと#requireの使い分け

Swift Testingでは、アサーションに#expectマクロと#requireマクロを使用します。

#expect

#expectは条件が満たされなくてもテストの実行を続行します。複数の検証を行いたい場合に便利です。

@Test func multipleExpectations() {
    let user = User(name: "田中", age: 25)
    
    #expect(user.name == "田中")
    #expect(user.age == 25)
    #expect(user.isAdult)
}

#require

#requireは条件が満たされない場合、即座にテストを終了します。後続の処理が実行できない場合に使います。また、オプショナルのアンラップにも使えます。

@Test func requireExample() throws {
    let json = """
    {"name": "田中", "age": 25}
    """
    
    let data = try #require(json.data(using: .utf8))
    let user = try JSONDecoder().decode(User.self, from: data)
    
    #expect(user.name == "田中")
}

#requireでオプショナルをアンラップすると、失敗時に適切なエラーメッセージが表示されるため、XCTUnwrapの代替として使えます。

パラメタライズドテスト

Swift Testingの目玉機能の一つが、パラメタライズドテストです。同じロジックを複数の入力値でテストしたい場合に非常に便利です。

パラメタライズドテストの概念図
@Test(arguments: [1, 2, 3, 4, 5])
func isPositive(value: Int) {
    #expect(value > 0)
}

複数のパラメータを組み合わせることも可能です。

@Test(arguments: [
    (input: "hello", expected: 5),
    (input: "swift", expected: 5),
    (input: "", expected: 0)
])
func stringLength(input: String, expected: Int) {
    #expect(input.count == expected)
}

XCTestで同等のことを実現しようとすると、ループを書くか、複数のテストメソッドを用意する必要がありました。Swift Testingでは1つのテストで複数のケースを簡潔に表現でき、それぞれが独立したテストケースとしてレポートに表示されます。

@Suiteによるテストのグループ化

Swift Testingでは、@Suiteマクロを使ってテストをグループ化できます。

@Suite("ユーザー認証テスト")
struct AuthenticationTests {
    @Test("正しいパスワードでログインできる")
    func loginWithCorrectPassword() {
        let auth = Authenticator()
        #expect(auth.login(password: "correct123"))
    }
    
    @Test("間違ったパスワードでログインできない")
    func loginWithWrongPassword() {
        let auth = Authenticator()
        #expect(!auth.login(password: "wrong"))
    }
}

@Suiteは入れ子にすることも可能で、テストを論理的に整理できます。

@Suite("算術演算テスト")
struct ArithmeticTests {
    @Suite("加算")
    struct AdditionTests {
        @Test func positiveNumbers() {
            #expect(2 + 3 == 5)
        }
        
        @Test func negativeNumbers() {
            #expect(-2 + -3 == -5)
        }
    }
    
    @Suite("乗算")
    struct MultiplicationTests {
        @Test func positiveNumbers() {
            #expect(2 * 3 == 6)
        }
    }
}

XCTestとの主な違い

ここまで見てきた機能を踏まえて、XCTestとの主な違いを整理します。

項目 XCTest Swift Testing
テスト関数の定義 func testプレフィックス @Testマクロ
アサーション XCTAssert系メソッド #expect / #require
テストのグループ化 クラス継承 @Suiteマクロ
パラメタライズドテスト ループで実装 argumentsパラメータ
セットアップ setUp() / tearDown() init() / deinit()
並列実行 デフォルトで並列 デフォルトで並列

特に大きな違いは、XCTestがクラスベースなのに対し、Swift Testingは構造体で書けることです。これにより値型のメリットを活かしたテストが書けます。

既存プロジェクトからの段階的移行

XCTestで書かれた既存のテストをSwift Testingに移行する場合、一度に全てを書き換える必要はありません。両者は同じテストターゲット内で共存できます。

移行の手順

まず、新しいテストファイルでSwift Testingを使い始めます。

// NewFeatureTests.swift
import Testing

@Suite("新機能のテスト")
struct NewFeatureTests {
    @Test func basicFunctionality() {
        // 新しいテストはSwift Testingで書く
    }
}

既存のXCTestは必要に応じて徐々に書き換えていきます。

注意点

  • Swift TestingはiOS 17.0以降が必須です。それ以前のOSをサポートする場合、XCTestを使い続ける必要があります
  • XCUITest(UIテスト)はSwift Testingに対応していないため、引き続きXCTestを使用します
  • 非同期テストはasync/awaitをそのまま使えます
@Test func asyncOperation() async throws {
    let result = try await fetchData()
    #expect(result.count > 0)
}

Swift Testingを使うメリット

最後に、Swift Testingを採用するメリットをまとめます。

記述量の削減: @Testマクロと#expectにより、ボイラープレートが減ります。テストの本質である「何をテストしているか」がコードから読み取りやすくなります。

パラメタライズドテスト: 同様のテストを複数の入力値で実行したい場合、非常に簡潔に書けます。テストケースの追加も容易です。

エラーメッセージの改善: #expectの失敗時には、条件全体が評価されて詳細なメッセージが表示されます。何が間違っていたのかが分かりやすくなっています。

構造体ベース: クラスではなく構造体でテストを書けるため、テスト間の状態共有による不具合が起きにくくなります。

まとめ

Swift TestingはSwiftの言語機能を活用した、よりモダンなテストフレームワークです。@Testマクロでテストを定義し、#expect/#requireでアサーションを書くという基本を押さえれば、すぐに使い始められます。

XCTestとの共存が可能なので、新規にテストを書く際にSwift Testingを採用し、既存のテストは必要に応じて徐々に移行していく、という段階的なアプローチがおすすめです。

パラメタライズドテストや@Suiteによるグループ化など、XCTestにはなかった便利な機能もあるので、ぜひ試してみてください。

出典一覧