Swift Testing は、Swift 5.9 で導入された Macro システムを前提に設計されています。これにより、以下のことが実現できます。
失敗時の情報が圧倒的に豊富になる
XCTest の XCTAssertEqual(a, b) が失敗した場合、出力は「XCTAssertEqual failed: ("5") is not equal to ("6")」程度です。Swift Testing の #expect(a == b) は、a と b の実際の値だけでなく、式の構造まで展開して表示します。複雑な条件式でも「どの部分が失敗したか」が一目でわかります。
テスト関数のオーバーヘッドがなくなる
XCTest では XCTestCase を継承したクラスが必須で、テスト関数名は test で始める必要がありました。Swift Testing では @Test アトリビュートを付けるだけで、どの関数でもテストになります。構造体(struct)でテストを書けるので、クラスの継承ヒエラルキーに縛られません。
import Testing// テスト対象のコードstruct Calculator { func add(_ a: Int, _ b: Int) -> Int { a + b } func divide(_ a: Double, _ b: Double) throws -> Double { guard b != 0 else { throw CalculatorError.divisionByZero } return a / b }}enum CalculatorError: Error, Equatable { case divisionByZero}// Swift Testing でのテスト@Suite("Calculator Tests")struct CalculatorTests { let calculator = Calculator() @Test("加算が正しく計算される") func addition() { let result = calculator.add(3, 5) // #expect は失敗時に a=3, b=5, result=? を全て展開して表示する #expect(result == 8) #expect(result > 0) } @Test("ゼロ除算でエラーがスローされる") func divisionByZero() { // エラーがスローされることを検証する #expect(throws: CalculatorError.divisionByZero) { try calculator.divide(10, 0) } } @Test("正常な除算が成功する") func normalDivision() throws { // #require はテストを即中断する(アサーション失敗で続行しない) let result = try #require(try? calculator.divide(10, 2)) #expect(result == 5.0) #expect(result.isFinite) }}
期待する出力:
Test Suite 'Calculator Tests' started
✓ 加算が正しく計算される (0.001 seconds)
✓ ゼロ除算でエラーがスローされる (0.001 seconds)
✓ 正常な除算が成功する (0.001 seconds)
Test Suite 'Calculator Tests' passed (0.003 seconds)
// テスト対象: 非同期のデータ取得サービスactor NetworkDataService { private var cache: [String: Data] = [:] func fetchData(from url: String) async throws -> Data { // キャッシュヒット if let cached = cache[url] { return cached } // 実際のネットワーク処理(テストではモックに差し替える) guard let validURL = URL(string: url) else { throw NetworkError.invalidURL(url) } let (data, response) = try await URLSession.shared.data(from: validURL) guard let httpResponse = response as? HTTPURLResponse, httpResponse.statusCode == 200 else { throw NetworkError.badResponse } cache[url] = data return data }}enum NetworkError: Error, Equatable { case invalidURL(String) case badResponse}// テスト用プロトコルを用意してモックに差し替える(依存性注入パターン)protocol DataFetching: Sendable { func fetchData(from url: String) async throws -> Data}struct MockDataFetcher: DataFetching { let result: Result<Data, Error> func fetchData(from url: String) async throws -> Data { try result.get() }}// Antigravity が生成したテストをベースにしたもの@Suite("Non-blocking Data Fetch Tests")struct DataFetchTests { @Test("有効な URL からデータを取得できる") func fetchValidData() async throws { let expectedData = "hello".data(using: .utf8)! let fetcher = MockDataFetcher(result: .success(expectedData)) let result = try await fetcher.fetchData(from: "https://example.com") #expect(result == expectedData) } @Test("無効な URL はエラーをスローする") func fetchInvalidURL() async { let fetcher = MockDataFetcher(result: .failure(NetworkError.invalidURL("not-a-url"))) await #expect(throws: NetworkError.invalidURL("not-a-url")) { try await fetcher.fetchData(from: "not-a-url") } } @Test("複数の並行フェッチが独立して実行される") func concurrentFetch() async throws { let fetcher = MockDataFetcher(result: .success(Data())) // withTaskGroup で並行テスト try await withThrowingTaskGroup(of: Data.self) { group in for i in 0..<5 { group.addTask { try await fetcher.fetchData(from: "https://example.com/\(i)") } } var results: [Data] = [] for try await data in group { results.append(data) } #expect(results.count == 5) } }}
await #expect(throws:) の形式が重要です。非同期の throws 関数に対しては、await を #expect(throws:) の前に置きます。これを忘れると「expression of type 'Bool' is unused」という紛らわしいエラーが出ることがあります。
Actor を使ったコードのテストでは、Swift Concurrency の isolation rules に従う必要があります。テスト関数自体も async にすることで、Actor との通信が自然な形でできます。
XCTest との共存と移行戦略
既存プロジェクトで XCTest から Swift Testing へ移行するとき、「全部一気に書き換えよう」としてはいけません。経験上、この「一気に書き換え」は高確率で頓挫します。
推奨する段階的移行アプローチ
フェーズ 1: 新規テストは Swift Testing で書く(今日から)
既存の XCTestCase を触らず、新しく追加するテストは全て Swift Testing で書くというルールを設定します。これはゼロコストで始められます。重要なのは、同じ Xcode プロジェクト内で XCTest と Swift Testing は共存できるという点です。
Swift Testing への移行は、次の新しいテストファイルを作るときが絶好のタイミングです。import Testing と書いて、@Test を付けてテスト関数を書く——それだけで始められます。
既存の XCTest は触らず、新しいテストから Swift Testing で書き始めてください。Antigravity に「このクラスに対して Swift Testing 形式でテストを生成して」と依頼すると、最初のコードが数秒で出てきます。それを確認・修正して動かす、この繰り返しで自然に手が動くようになります。