【iOS】Swift Testingで単体テストの実装方法と#expect/#requireの使い方!

この記事からわかること
- XcodeでSwift Testingの使い方
- 単体テストの実装方法
- #expect/#require検証マクロの使い方
index
[open]
\ アプリをリリースしました /
環境
- Xcode:16.3
- iOS:18.0
- Swift:5.9
- macOS:Sequoia 15.4
iOSアプリにおけるテスト
iOSアプリ開発においてテスト機能は統合開発環境であるXcodeに内包されています。その中でも「XCTest」と「Swift Testing」の2種類があり今回は「Swift Testing」の使い方についてまとめていきます。「XCTest」やその他以下のことは別記事でまとめているので参考にしてください。
- テストの必要性
- UI Testとは?
- 単体テスト(Unit Test)とは?
- XCTestの使い方
XCTestとSwift Testingの違いや特徴は以下の記事が参考になりました。
おすすめ記事:Swiftの新テストライブラリ「swift-testing」特徴と導入
Swift Testing
「Swift Testing」は「XCTest」に変わるテスト機能です。以前からライブラリとして公開はされていましたがXcode16以降から標準で組み込まれるようになりました。既存でXCTest存在しても並行して動作させることができるため新規プロジェクトでなくてもこれから活用していけるようになっています。
GitHub:swiftlang / swift-testing
Swift Testingを導入する
新規プロジェクトにSwift Testingを導入したい場合は「Testing System」で「Swift Testing with XCTest UITests」を選択します。

プロジェクト作成後からテストを導入するためにはXcodeの上部メニューから「File」 > 「New」 > 「Target...」をクリックし、Test内の「UI Testing Bundle」または「Unit Testing Bundle」を選択、「Testing System」を「Swift Testing」を選択し「NEXT」をクリックします。

テスト関数の定義
公式リファレンス:Defining test functions
Swift Testingを導入するとプロジェクトに新しいターゲットとして追加されます。デフォルトでテスト用の構造体が記述されたファイルが生成されます。Swift Testingでは構造体struct
やアクターactor
、final class
などSwift6での並行性のチェックでも問題のないような設計で実装することが推奨されています。
import Testing
struct StructTest {
@Test func example() { }
}
actor ActorTest {
@Test func example() {}
}
この中に@Test
アノテーショションを付与した関数を定義しテストしたい内容をその中に記述していきします。検証には#expect
や#require
などのアサーションマクロ(※)を使用していきます。
※ マクロとはコンパイル時にコードを自動生成する機能です。#expect
を使用することでテストの失敗を検知する処理として動作させることができます。
#expect
テストでの値の検証には#expect
マクロを使用し、引数に検証したい結果を真偽値で渡します。テストメソッド内には検証したい数だけ#expect
を記述することが可能で、失敗してもテストは終了せず、後続の処理が実行されます。
@Test func animalTest() {
// 複数検証してもOK
let result = "Cat"
print("------1")
#expect(result == "Cat")
print("------2")
// 失敗(false)しても後続の処理は行われる
#expect(result.contains("S")) // Expectation failed: (result → "Cat").contains("S" → S)
print("------3")
#expect(result.contains("C"))
print("------4")
}
◇ Test run started.
↳ Testing Library Version: 102 (arm64-apple-ios13.0-simulator)
◇ Suite SampleAppTests started.
◇ Test animalTest() started.
------1
------2
✘ Test animalTest() recorded an issue at ClipImageAppTests.swift:17:9: Expectation failed: (result → "Cat").contains("S" → S)
------3
------4
✘ Test animalTest() failed after 0.001 seconds with 1 issue.
✘ Suite SampleAppTests failed after 0.001 seconds with 1 issue.
✘ Test run with 1 test failed after 0.002 seconds with 1 issue.
XCTestではXCTAssertEqual
やXCTAssertNotNil
など数多くの検証用メソッドが用意され、適切な場面で適切なメソッドを使用する必要がありましたが、Swift Testingなら#expect
1つでまかなうことが可能になっているので学習コストが低く、初学者でも使いやすくなっています。
テストを実行するにはテストごとに「開始ボタン」が左側に表示されるのでクリック、またはCommand + U
で実行することができます。実行するとビルドでシミュレーターが立ち上がりテストが実行されデバッグログにテスト結果が出力されます。
◇ Test run started.
↳ Testing Library Version: 102 (arm64-apple-ios13.0-simulator)
◇ Suite SampleAppTests started.
◇ Test animalTest() started.
✔ Test animalTest() passed after 0.001 seconds.
✔ Suite SampleAppTests passed after 0.001 seconds.
✔ Test run with 1 test passed after 0.001 seconds.

#require
似たような検証マクロに#require
があります。基本的な使い方は#expect
と同じですが失敗した場合にテストが終了し後続の処理を実行しない特徴があります。また値がnil
の場合でも失敗扱いとなるので、意図しないnil
を検知することも可能です。呼び出す際は例外ExpectationFailedError
を投げる可能性があるのでtry
を使用し、do〜catch
でエラーを補足することができます。
@Test func animalNullTest() {
print("------1")
let result: String? = nil
do {
print("------2")
// 失敗すると後続の処理は行わない
let nonNull = try #require(result)
print("------3")
#expect(nonNull != nil)
} catch {
// 失敗はキャッチできる
print("ERROR:", error)
}
}
◇ Test run started.
↳ Testing Library Version: 102 (arm64-apple-ios13.0-simulator)
◇ Suite SampleAppTests started.
◇ Test animalNullTest() started.
------1
------2
✘ Test animalNullTest() recorded an issue at SampleAppTests.swift:29:31: Expectation failed: result → nil
ERROR: ExpectationFailedError(expectation: Testing.Expectation(evaluatedExpression: Testing.__Expression.Kind.generic("result"), mismatchedErrorDescription: nil, differenceDescription: nil, mismatchedExitConditionDescription: nil, isPassing: false, isRequired: true, sourceLocation: SampleAppTests/SampleAppTests.swift:29:31))
✘ Test animalNullTest() failed after 0.006 seconds with 1 issue.
✘ Suite SampleAppTests failed after 0.006 seconds with 1 issue.
✘ Test run with 1 test failed after 0.006 seconds with 1 issue.
@Testアノテーション
@Test
の引数には文字列を渡すことでXcodeのログやコマンドライン上で表示されるテスト名をカスタマイズすることができます。
@Test("動物名確認テスト")
func animalTest() {
let result = "Cat"
#expect(result == "Cat")
}
他にも例外を投げるthrows
、非同期にするためのasync
などを付与することも可能です。
@Test func animalTest() async throws { ... }
パラメタライズテスト
@Test
の引数arguments
にテストメソッドのパラメーターに渡したい値をコレクション形式などで渡すことで複数の値でのテストを自動で実行させることが可能です。
@Test(arguments: ["Cat", "Dog", "Lion"])
func animalTest(animal: String) {
#expect(animal == "Cat")
}
@Test(arguments: 1...99)
func someIntTest(_ value: Int) {
#expect(int > 50)
}
メインスレッドでの実行
Swift Testingのテスト処理は基本的に任意のスレッドで実行されます。そのため意図的にメインスレッドで実行したい場合は@MainActor
を付与してスレッドを調整してあげる必要があります。
@Test() func threadCheck() {
// Expectation failed: (Thread → NSThread).isMainThread → false
#expect(Thread.isMainThread)
}
@Test()
@MainActor
func threadCheckMain() {
// 成功:メインスレッドで実行される
#expect(Thread.isMainThread)
}
@Suiteアノテーション
@Suite
はテストをグループ化し同時に実行できるようにするための仕組みです。これにより複数のテストクラスを一括で管理することができ、テストクラスの責務の細分化と可読性を向上させることができるようになります。
定義の中にテスト関数があれば明示的に@Suite
を記述しなくても暗黙的にスイートとして識別されます。@Suite
で明示的にスイートにしたいときはテスト関数がなく、複数のスイートなどを1つにまとめたい時になります。
// 複数のスイートやテストケースを包括したスイート
@Suite
struct AllTests {
static let allTests = [
UserTests.self,
MathTests.self,
NetworkTests.self
]
}
// @Suiteを宣言しなくても暗黙的にスイートとして識別される
struct UserTests {
@Test func someTest() { }
}
@Suite
を付与したオブジェクトはテスト関数(static
またはclass
キーワードのないもの)があれば実行します。
また引数に文字列を渡すことでスイート名をつけることも可能です。
@Suite("ユーザー登録テスト")
まだまだ勉強中ですので間違っている点や至らぬ点がありましたら教えていただけると助かります。
ご覧いただきありがとうございました。