ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
Swift Testingの詳細
Swift Testingの組み込みの機能を使用して、優れた(テスト)スイートのセットを記述する方法を習得しましょう。Swift Testingの構成要素をさらに活用してテストを拡張し、幅広いシナリオへの対応、さまざまなスイート全体でのテストの整理、テストの並行実施時の最適化を推進する方法を解説します。
関連する章
- 0:00 - Introduction
- 0:36 - Why we write tests
- 0:51 - Challenges in testing
- 1:21 - Writing expressive code
- 1:35 - Expectations
- 3:58 - Required expectations
- 4:29 - Tests with known issues
- 5:54 - Custom test descriptions
- 7:23 - Parameterized testing
- 12:47 - Organizing tests
- 12:58 - Test suites
- 13:33 - The tag trait
- 20:38 - Xcode Cloud support
- 21:09 - Testing in parallel
- 21:36 - Parallel testing basics
- 24:26 - Asynchronous conditions
- 26:32 - Wrap up
リソース
- Adding tests to your Xcode project
- Forum: Developer Tools & Services
- Improving code assessment by organizing tests into test plans
- Running tests and interpreting results
- Swift Testing
- Swift Testing GitHub repository
- Swift Testing vision document
関連ビデオ
WWDC24
WWDC21
-
ダウンロード
こんにちは Swift Testingチームの Jonathanです 今日は同僚のDorothyと一緒に Swift Testingのパワフルな機能をご紹介し 皆さんのテスト開発を 次のレベルに引き上げていきたいと思います Swift Testingは最新の オープンソースのテストライブラリで パワフルで豊かな表現力を備えています この機能はXcode 16に含まれています まだご覧になっていない方は 「Meet Swift Testing」を視聴して Swift Testingの基本をご確認ください
テストは開発プロセスにおける 重要なステップです コードがユーザーに届く前に 問題を明らかにし 自信を持って 質の高い製品を 提供できるようになります しかしプロジェクトで 増え続ける テストを維持する際には 課題に直面するかもしれません テストはコードの動作を文書化し 強制します コードが複雑になるほど テストを読みやすく 理解しやすくすることが重要です コードに含まれる潜在的な あらゆるエッジケースに対応するには 多くの考慮と努力が必要です 多数のテストを整理し 関連付けることは複雑な作業であり テスト間の隠れた依存関係は テストを脆弱にし 予期せぬ失敗が生じやすくなります それでは Dorothyにバトンタッチします テストの読みやすさは重要です 読みやすいテストは扱いやすく 特にコードが複雑になったときに テストの失敗をより理解しやすくなります Swift Testingには 様々な機能が含まれており 明確で表現力豊かなテストを 記述するのに役立ちます 期待値を使用すると Swift Testingでコードが期待通りに 動作するかどうかを検証できます 期待値は Swiftの言語機能と構文を利用して 非常に表現力豊かで 簡潔なインターフェイスを提供します
こちらのサンプルは 簡単なtrue/falseの評価式を使った 期待値の例です しかし expectマクロはさらにパワフルで より複雑な検証にも対応できます エラーハンドリングは多くの場合 テストされませんが ユーザー体験において 重要な位置付けになります 無効な入力や予期せぬ状況に対して コードが適切に失敗することを 確認することが重要です expect throwsマクロは 期待値を基にして この作業を非常に簡単にします コードの理想的な経路をテストして 例外をスローする関数が 問題なく戻ることを期待している場合は テスト内でその関数を呼び出します 関数がエラーをスローした場合 テストは失敗します 関数が正常に実行され 値が返されれば 期待値を使って それが想定通りであることを確認できます 一方 失敗したケースが想定通りに 動作することを確認するには 関数がスローしたエラーを キャッチして調べる必要があります do catchステートメントを自分で追加して エラーを調べることもできますが 非常に冗長になりますし エラーがスローされなかった場合 このコードでは うまくいったかどうかがわかりません そこでSwift Testingが expect throwsマクロで支援します 自分でdo catchステートメントを 書く代わりに expect throwsマクロが その大変な作業を行います brew関数がエラーをスローすれば テストは成功です エラーがスローされなければ テストはです
特定のタイプのエラーが スローされることを確認したい場合は そのタイプを 任意のErrorの代わりに 渡すことができます このテストは エラーが発生しない場合や BrewingErrorのインスタンスでない エラーが発生した場合は 失敗します さらに一歩進めて 特定のエラーがスローされたかどうかを 検証することもできます 最も複雑なケースでは expect throwsマクロを使って カスタマイズした検証を行うことができます エラー検証をカスタマイズして 特定のエラータイプや ケースをチェックしたり 関連する値やプロパティが 正しいかどうかを確認したり その他必要なことを行って コードがスローしたエラーが適切かどうかを 確認することができます 「Meet Swift Testing」のセッションでは Stuartが 必須のexpectationの概念を ご紹介しました 簡単におさらいすると throwを含むものを含めて 標準の期待値は 必須のexpectationにすることができます オプショナル値を検証する場合には 必須のexpectationを使って コントロールフローを文書化できます
値がnilになる場合には 調査する意味がないので 他にすべきことがなければ 必須のexpectationを追加して テストを早めに終了させることができます 次に withKnownIssue関数を使って テストで既知のエラーを文書化する方法を 説明していきます テスト失敗のトリアージは 時間のかかるプロセスです 失敗したテストが すぐに修正できない場合や コントロールできない要因で 失敗した場合 その失敗はテスト結果にノイズを加えて 実際のユーザーにとっての問題を 覆い隠してしまう可能性があります このテストは ソフトクリームの機械が ソフトクリームのコーンを 作れるかを確認していますが 現在機械が故障しているため テストが失敗しています 修理技師が機械を修理するまで 時間がかかるかもしれません 技師の到着を待つ間 まずこのテストにはdisabledトレイトを 使おうと思うかもしれません しかしこの場合は withKnownIssueを 使うのが良い選択です テストは引き続き実行され コンパイルエラーが通知されます 関数がエラーを返した場合 そのテスト結果は テスト失敗としてカウントされません 想定されたことだからです 代わりに そのテストは結果の中で 想定された失敗として表示されます 問題が解決され エラーがスローされなくなると 通知されますので withKnownIssueの呼び出しを削除して 通常通りテストを実行することができます テストで複数のチェックが 実行されることもあります
この場合 問題が生じている関数を withKnownIssueでラップするだけで 残りの検証を実行することができます このセクションの最後に カスタムテストの説明について見ていきます 理想としては すべてのテストが常に合格し ソフトクリームの機械も常に 問題なく動くことが望まれますが 現実の世界では テストの失敗が発生します カスタムテストの説明を見ると テストの中で何が起きているかを一目で把握し 問題が発生したときには 解決への手がかりを得ることができます シンプルな列挙型を扱う場合 デフォルトの説明は通常 簡潔で明確です しかし 構造体やクラスなどの より複雑な型は デフォルトで多くの情報を含むため ノイズが多くなります テスト中に役立たない余計な データが含まれることがあるのです
これらの値は多くの情報を伴って Xcodeに表示されます 情報は正確ですが それぞれの値を区別する重要な部分を 見つけることが 難しい場合があります その場合は それぞれのテストに 簡単な説明を付けることをお勧めしますが 製品コードには影響が出ないようにします CustomTestStringConvertible プロトコルに準拠させることで テスト専用のカスタマイズされた 説明を提供できます これで Test Navigatorや Test Reportで より読みやすく 説明的な値を得ることができます ここまでに スローされたエラーの処理 必須のexpectationで テストを早期終了する方法 既知の問題の処理 そしてテスト出力を 読みやすくする方法を見てきました これで あらゆる状況に対応できるテストの 準備が整いました それではJonathanに戻ります 先ほど述べたように コードの品質を保つ上での課題の1つは すべてのエッジケースをカバーすることです 日常のテストではめったに発生しない 複雑な機能の部分です さまざまな条件下でテストを行い エッジケースを見つけだすのは よいことですが 従来はそれに多大な時間を要していました また すべてのバリエーションごとに 個別のテストを記述するのは メンテナンスの悪夢です Swift Testingを使えば 1つのテスト関数を 様々な引数で簡単に実行できます どういうことかお見せします この列挙型には様々なアイスクリームの フレーバーがリストアップされており 特定のフレーバーにナッツが含まれているか どうかを確認できます containsNutsプロパティでは 列挙型のすべてのケースを テストする必要があります Swift Testingの パラメータ化されたテストを使えば そのテストカバレッジを簡単に追加できます
このテスト関数は 列挙型のケースに ナッツが含まれるかどうかを確認します この場合 バニラにナッツが含まれています 列挙型の各ケースに対して 個別のテスト関数を書くこともできますが コードが多くなります 同じ関数を何度もコピーして貼り付けると 誤って 間違った値を チェックしてしまいがちです ここで本当に必要なのは 1つか2つのテスト関数だけです 結局 テストのロジックは同じで 異なるのは1つの入力値だけです それがパラメータ化されたテストです 1つまたは複数の パラメータを取るテストです テスト関数が実行されると Swift Testingは 自動的にそれを分割し 引数ごとに1つずつ 個別のテストケースにします
これらのテストケースは 完全に独立しており 並行して実行できます これにより forループや 個別のテスト関数を使用するよりも すべてのテストを 実行する時間が短くなります Xcodeは テスト関数の個別の テストケースの再実行をサポートします 入力の型がCodableに 準拠している場合です それにより 個別の失敗した テストケースを再試行することができ 成功した他のテストケースを 再実行する必要がなくなります では テスト関数をパラメータ化する方法を 詳しく見ていきます まず 列挙型のすべてのケースをループして それぞれをテストする テスト関数を記述します この関数は機能しますが 改良の余地があります ここで問題が1つ発生します このテスト関数が 配列内の1つの値で失敗すると 実行が停止され その後の引数はテストされなくなります どの値が失敗したのかは不明であり 求めるカバレッジを得ることができません この場合 テスト関数内で 反復処理するのではなく 入力を上に移動して テスト属性に渡すことができます コレクションを パラメータとしてテスト属性に渡すと テストライブラリは そのコレクションの各要素を 1つずつ テスト関数の最初かつ唯一の 引数として渡します その後 それらの引数の1つで テストが失敗した場合は 対応する診断情報が どの入力に注意が必要かを 明確に示してくれます これでほぼ終了です ナッツを含むフレーバーをテストする 別の関数を追加すれば この列挙型のコードカバレッジが 100%になります 将来 列挙型を拡張する場合も 新しいテストケースを簡単に追加できます
ここでは列挙型のケースを見てきましたが パラメータ化されたテスト関数は 他の多くの種類の入力も 受け付けることができます 配列 辞書 範囲などの 送信可能なコレクションはすべて テスト属性に渡すことができます
テストケースとその引数は Test NavigatorとTest Reportの 両方に表示されます ナビゲータでは 各引数に個別の実行ボタンが付き テストレポートでは パラメータ化されたテストが失敗した場合に 豊富な情報ビューが表示されます ここまで 1つの入力を持つパラメータ化 されたテストの書き方を学びましたが より多くの入力を渡す場合は どうでしょうか Swift Testingのテスト関数は 複数の入力を受け入れることができ このテストの最初の引数の後に 別の引数を追加するだけで済みます 最初のコレクションの各要素は テスト関数の最初の引数として渡され 2番目のコレクションの各要素は 2番目の引数として渡されます この2つのコレクション要素の すべての組み合わせが自動的にテストされます 1つのテスト関数ですべての 組み合わせをテストできるため テストカバレッジを 大きく高めることができます その強力さを視覚化するため 2つの引数の配列を考えてみましょう 原材料とそれで作れる料理です 2つの引数を持つテスト関数で すべての組み合わせがテストされます 合計16通りです おかしな組み合わせも 試すことになるかもしれません 私はエッグサラダやオムライスは 好きですが レタスフライってなんでしょうか? よくわかりません そして 各配列には 4つの値しか含まれていません 2つのセットにさらに入力を追加すると テストケースの数は増え続け 指数関数的に増加します この急激な増加を コントロールするため テスト関数は最大2つの コレクションを受け付けます Swift標準ライブラリのzip関数を使用して 一緒にするべき入力ペアを マッチさせることもできます 原材料と最終的な料理の すべての組み合わせをテストする代わりに zipを呼び出してペアにします Zipはタプルのシーケンスを生成し 最初のコレクションの各要素が 2つ目のコレクションの対応する要素と ペアになり それ以外は含まれません これでパラメータ化されたテストで テストカバレッジを広げるための 必要なツールが揃いました ここからはもう一度Dorothyが テストの整理についてお話しします こうした新機能を使うと 様々なテストを記述しやすくなりますが それらを管理するための戦略も必要です Swift Testingが提供するツールを使って テストを整理する方法を見ていきましょう まとめると スイートはテスト関数を含む型です その機能は 表示名などのトレイトで 文書化することができます Swift Testingでは スイートに 他のスイートを含めて より柔軟にテストを整理することができます これは非常に素晴らしいテストセットですが あまりわかりやすく整理されていません このテストスイートには 温かいデザートのテストと 冷たいデザートのテストが含まれています サブスイートを追加することで テスト自体にこの構造を反映させ これらのテストグループ間の関係を より明確にすることができます タグもテストを整理するもう一つのトレイトです 複雑なパッケージやプロジェクトには 何百何千ものテストとスイートが 含まれている可能性があり コードの様々な部分をカバーする複数の テストスイートが存在する場合があります 直接関連していなくても テストの一部のサブセットは 共通の特性を共有している場合があります この例では いくつかのテストは カフェインを含む食品に関連し 別のいくつかのテストは チョコレートを含む食品に関連しています この場合 タグを使ってこれらのテストを 2つのスイートに関連付けることができます 重要な点としてタグはテストスイートの 代わりではないことが挙げられます スイートはソースレベルで テスト関数に構造を与えますが タグは共通点を持つ異なるファイル スイート そしてターゲットのテストを 関連付けるために役立ちます 以上がタグの概要ですが タグを宣言し テストに追加する方法はどうなるでしょうか これらの飲み物はすべてカフェインを含みます エスプレッソブラウニーも同様です これらのテストを関連付けるため カフェインのタグを作成します これらは別のスイートに存在します まず Tag型を拡張し caffeinatedという名の 静的変数を宣言します その変数は Tagのインスタンスである 必要があります そして秘密の材料に関して 変数にタグ属性を追加し テストでタグとして使用できるようにします
タグを作成したので それをこれらのテストに追加できます DrinkTestsの スイートレベルに追加します これらのテストで使用される飲み物は すべてカフェインを含んでいるので テストはスイートからタグを継承します 次に そのタグを espressoBrownieTextureに追加します これはDessertTestsで唯一カフェインを 含む食品であるため DessertTestsのスイート全体に 追加することはできません
スイートとテストには 複数のタグを持たせることもできます 例えば モカとエスプレッソブラウニーは どちらもカフェインを含みますが いずれもチョコレートで作られています チョコレートのタグを作成して この2つのテストに追加することができます Test Navigatorでは タグごとにテストがグループ化され 特定のタグを持つテストを 実行することができます では Xcode 16でタグを使用する方法を 見ていきましょう Test Navigatorには タグ付きテストを扱うための いくつかの新機能があります ナビゲータはデフォルトで テストを ソースコードの位置で整理して表示します 私はずっと秘密のホットソースのレシピを 完成させるべく取り組んでおり それを使用するコードのテストカバレッジを 改善したいと思っています これらのテストはすでに作成済みです Test Navigatorの下部にある フィルタフィールドを使用して ホットソースに関連するテストを見つけます 入力を始めると Xcodeはプロジェクトで 利用可能なタグに基づいて タグを提案します ここでXcodeは seasonal spicy street foodなど いくつかのタグを提案しています 入力を続けて結果を絞り込みます デフォルトでは フィルタフィールドは テストの表示名と関数名にマッチします そのためや などのテストが表示されています Xcodeが提案しているテストには 強調表示された単語が名前に 含まれないものもあります これは入力内容とマッチする タグがあるからです ポップオーバーの候補で spicyをクリックすると Test Navigatorは入力した内容を タグフィルタに変換し そのタグがないテストをすべて削除します これでTest Navigatorには spicyタグのある テストのみが表示されるようになります Test Navigatorはテストをタグごとに グループ化することもできます このビューに切り替えるには Test Navigatorの上部にある タグアイコンをクリックします タグフィルタを削除して プロジェクト内のすべてのタグが 結果に表示されるようにします このビューはテストを実行する際に便利です 階層ビューと同様に 任意のタグの横の再生ボタンをクリックすると そのタグのすべてのテストを実行できます 開発中に ホットソース関連の テストを実行し 変更についての フィードバックを素早く得ることができます これらのテストを手動で 実行することに加えて 再設計された テストプランエディタを使用すると タグの設定を テストプランに 保存することができます 信頼性の高いテストのコアセットを含む 新しいテストプランを作成しました これにより 変更を加えたときに発生する 可能性のあるバグを迅速に検出できます このテストプランには すべての テストターゲットが含まれています Test Navigatorのテストプランリストから Core Foodという名前を選択して 新しいテストプランに 切り替えることができます 次に テストプランエディタを開きます Test Navigatorで その名前を直接クリックします ユニットテストのターゲットを展開して すべてのテストを表示します 周りを見回すため ナビゲータを非表示にして スペースを少し広げます おさらいです テストプランは 1つ以上の テストターゲットを参照することができ テストプランエディタはそれらのターゲット 全体のテストを整理できます
各スイートおよびテストのタグは 右側の列に表示されます
これらのフィールドにタグを指定すると 含めるテストや 除外するテストを選択できます 例えばこのテストプランを すべての コアテストを実行するように更新する場合は seasonalタグのあるテストを除外します これらのテストは 年間の特定の時期にのみ 機能する予定のコードを対象としているため 常に実行する必要はありません excludeフィールドに そのタグを追加することで seasonalタグのあるテストを除外できます
テストプランのプレビューは 変更に合わせて自動的に更新されます 現在アクティブなタグは includeおよびexcludeフィールドで 紫色でハイライトされます テストプランから除外されたタグは 取り消し線で表示されます excludeフィールドに別のタグを追加すると 追加のフィルタオプションが表示されます テストプランが複数のタグで フィルタされる場合 Xcodeでは すべてのタグと任意のタグの どちらにマッチさせるかを選択できます デフォルトはすべてのタグです ここではseasonalまたは unreleasedタグのある テストをすべて除外したいので 任意のタグを選択します これで両方のタグが紫色でハイライトされ 両方とも取り消し線が引かれます これらのタグがテストプランから アクティブに除外されているからです タグはまた テストのターゲット全体にわたり 結果を分析する上で 役立つ便利なツールでもあります これは 先ほど作成した テストプランを実行した後に 生成されたTest Reportです かなりの数の失敗がありますので Test Reportがタグを使って 失敗を迅速に修正するために どのように役立つか確認してみましょう テストのアウトライン画面を 詳しく見てみます タグは今 対応するスイートおよびテストの 横のアウトラインに表示されています
タグフィルタを使うと 結果を絞り込むことができます しかし 失敗が多いので 1つずつ確認するのは面倒です
画面に移動すると 新しい分布インサイトのセクションがあり 共通のレポート先 タグ バグのある テスト失敗のパターンが表示されます このインサイトは興味深いですね spicyタグが付いた すべてのテストが失敗しています インサイトの行をダブルクリックすると 詳細画面に移動できます
関連するすべての 失敗したテストとそのメッセージが この画面に表示されます 最近 秘密のホットソースに使っている チリペッパーを変更しましたので そのせいでspicyのテストが すべて失敗したのかもしれません 変更を確認してテストを修正します
Xcode CloudもSwift Testingを サポートするようアップデートされました Xcodeと同様に App Store ConnectのXcode Cloudタブで テストスイートの結果を確認できます これには テストで定義した トレイトに関する詳細が含まれます テストを整理して関連付けたら それらに影響する問題についてのインサイトを Xcodeで得ることができます スイートとタグを利用すると 大規模なテストのコレクションの操作が 効率的になり 管理も容易になります テストを並行して実行する方法について Jonathanから説明してもらいましょう これでかなりの規模のテストスイートを 管理できるようになったので 並行テストによってどのように テストの迅速性を保ち 並行実行環境でどのように 信頼性の高いテストを実行できるかを 検討してみましょう Swift Testingでは並行テストが デフォルトで有効になっているため コードを追加することなく こうした機能を活用できます 今回初めて すべての物理デバイスで 並行テストを実行し その優れたメリットを いっそう多くのテストで 活用できるようになりました ここで 並行テストの基本を 説明したいと思います シリアルテストではテストが 一つずつ順番に実行されます XCTestを使ったことがあれば この方式がデフォルトでしたよね これに対して並行テストでは テストが同時に実行されます テストが並行して実行されると いくつかのメリットがあります
まず 実行時間が短縮されます これは毎分が貴重な 継続的インテグレーションでは特に重要で それにより 結果を迅速に得ることも可能になります Swift Testingは 同期的か非同期的かにかかわらず デフォルトでテスト関数を 並行して実行します この点はXCTestと大きく異なります XCTestは複数のプロセスを使用した 並行処理をサポートするだけであり 各プロセスが 一度に1つのテストを実行します テスト関数は 必要に応じて MainActorのような グローバルアクターに分離できます 次に テストの実行順序が ランダム化されます これにより テスト間の 隠れた依存関係が表面化し 調整が必要な箇所が明らかになります では例を見てみましょう 2つのテストがあります 1つ目でカップケーキを焼き 2つ目でそれを食べます これらのテストが常に順番に実行された場合 常に2つ目のテストのための カップケーキがあることになります 1つ目のテストで焼かれたものです これは意図した動作ではありません テストが並行して実行されると 1つ目のテストに対する2つ目のテストの 依存関係が実行時に明らかになるため 修正することができます 古いテストコードを変換する場合は こうした依存関係がすでに 組み込まれているかもしれません Swift 6は コードを書き直しながら 既存のコードの問題を見つける上では 有用ですが その他の問題は見つけにくくなります まず最初にコードを Swift Testingに変換し 後でそれらの問題に 対処することをお勧めします すぐにすべてを修正することは できないかもしれませんが ここで .serializedトレイトが役立ちます そのテストをシリアルに実行する必要が あることを示すには .serializedトレイトを テストスイートに追加します こうしたテストでは 先ほど説明したメリットが失われますので 可能であればいつでも並行実行できるように テストコードをまず リファクタリングすることを 検討してください .serializedはパラメータ化された テスト関数にも適用して テストケースが一度に1つずつ 実行されるようにできます 他のスイートを含むスイートに 適用された場合は 自動的に継承されるため 2回追加する必要はありません .serializedトレイトを持つスイート内の テストは 一つずつ実行されます しかし Swiftでは引き続き 他の無関係なテストを 並行して実行することができるため 並行パフォーマンスを維持できます 必要に応じて シリアルに実行することもできますが 並行してテストを実行できるように リファクタリングすることをお勧めします Swift Testingを使用すると 並行テストがデフォルトで有効になり テストを可能な限り早く実行できます Swift 6は 並行実行を妨げている テストの問題を 見つけるのにも役立ちます 次に Swift Testingを使用して 非同期条件で待機する技術をご紹介します 並行テストコードを書くときは 本番コードと同じ並行機能を Swiftで使用できます awaitもまったく同じように機能し テストを中断させます 作業が保留中の間は 他のテストコードが CPUを使用し続けることができます 特にCやObjective-Cで書かれた 古いコードでは 非同期操作の終了を通知するために 完了ハンドラが使用されることがあります このコードはテスト関数が戻った 後に実行され 関数が成功したかどうかは 確認できません ほとんどの完了ハンドラで Swiftは自動的に 非同期オーバーロードを提供します テストしているコードが 完了ハンドラを使用していて 非同期オーバーロードを利用できない場合は withCheckedContinuationや withCheckedThrowingContinuationを 使用して 待機できる式に変換することができます
Swiftの継続について さらに詳しく知りたい場合は 「Meet async/await in Swift」を ご覧ください
もう一つのコールバックの種類は 複数回 発生する可能性のあるイベントハンドラです このバージョンのeat関数は 食事全体の終わりではなく クッキーごとにコールバックを呼び出します しかし 食べたクッキーの数を 変数でカウントしようとすると Swift 6では並行エラーが発生します このように変数を設定するのは 安全ではないからです
テストしているコードがコールバックを 複数回呼び出す可能性があり 呼び出された回数を テストする必要がある場合は 代わりにconfirmationを使用します デフォルトでは confirmationは 一度だけ発生すると想定されますが 別の想定回数を指定することもできます 私は10個のおいしいクッキーを 焼いて食べているので このイベントは10回発生するものと 想定しています テスト中にconfirmationが 一度も発生しない場合は 0を指定することもできます Swiftの並行処理は 本番コードおよび テストコードにおいて強力なツールです テストを並行して実行して より早く結果を得るとともに async/await、継続 confirmationを使用することで テストコードを並行環境で 確実に正しく実行できます 今回は Swift Testingがテストワークフローを どのように改善するかを紹介しました 幅広いトピックをカバーしましたので 簡単に振り返りたいと思います まず Swift TestingのAPIが 表現力豊かなテストの記述に役立つこと そしてパラメータ化を使えば 1つのテストで 多くの異なるケースを実行できること また スイートやタグなどのツールを使えば テストコードを整理して文書化できること そして最後に テストを並行実行することで テストの実行時間を短縮し それらの間の 依存関係も特定できることを 説明しました ご視聴いただきありがとうございました テストをお楽しみください
-
-
0:01 - Successful throwing function
// Expecting errors import Testing @Test func brewTeaSuccessfully() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) let cupOfTea = try teaLeaves.brew(forMinutes: 3) }
-
0:02 - Validating a successful throwing function
import Testing @Test func brewTeaSuccessfully() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) let cupOfTea = try teaLeaves.brew(forMinutes: 3) #expect(cupOfTea.quality == .perfect) }
-
0:03 - Validating an error is thrown with do-catch (not recommended)
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 3) do { try teaLeaves.brew(forMinutes: 100) } catch is BrewingError { // This is the code path we are expecting } catch { Issue.record("Unexpected Error") } }
-
0:04 - Validating a general error is thrown
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: (any Error).self) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:05 - Validating a type of error
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: BrewingError.self) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:06 - Validating a specific error
import Testing @Test func brewTeaError() throws { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect(throws: BrewingError.oversteeped) { try teaLeaves.brew(forMinutes: 200) // We don't want this to fail the test! } }
-
0:07 - Complicated validations
import Testing @Test func brewTea() { let teaLeaves = TeaLeaves(name: "EarlGrey", optimalBrewTime: 4) #expect { try teaLeaves.brew(forMinutes: 3) } throws: { error in guard let error = error as? BrewingError, case let .needsMoreTime(optimalBrewTime) = error else { return false } return optimalBrewTime == 4 } }
-
0:08 - Throwing expectation
import Testing @Test func brewAllGreenTeas() { #expect(throws: BrewingError.self) { brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2) } }
-
0:09 - Required expectations
import Testing @Test func brewAllGreenTeas() throws { try #require(throws: BrewingError.self) { brewMultipleTeas(teaLeaves: ["Sencha", "EarlGrey", "Jasmine"], time: 2) } }
-
0:10 - Control flow of validating an optional value (not recommended)
import Testing struct TeaLeaves {symbols let name: String let optimalBrewTime: Int func brew(forMinutes minutes: Int) throws -> Tea { ... } } @Test func brewTea() throws { let teaLeaves = TeaLeaves(name: "Sencha", optimalBrewTime: 2) let brewedTea = try teaLeaves.brew(forMinutes: 100) guard let color = brewedTea.color else { Issue.record("Tea color was not available!") } #expect(color == .green) }
-
0:11 - Failing test with a throwing function
import Testing @Test func softServeIceCreamInCone() throws { try softServeMachine.makeSoftServe(in: .cone) }
-
0:12 - Disabling a test with a throwing function (not recommended)
import Testing @Test(.disabled) func softServeIceCreamInCone() throws { try softServeMachine.makeSoftServe(in: .cone) }
-
0:13 - Wrapping a failing test in withKnownIssue
import Testing @Test func softServeIceCreamInCone() throws { withKnownIssue { try softServeMachine.makeSoftServe(in: .cone) } }
-
0:14 - Wrap just the failing section in withKnownIssue
import Testing @Test func softServeIceCreamInCone() throws { let iceCreamBatter = IceCreamBatter(flavor: .chocolate) try #require(iceCreamBatter != nil) #expect(iceCreamBatter.flavor == .chocolate) withKnownIssue { try softServeMachine.makeSoftServe(in: .cone) } }
-
0:15 - Simple enumerations
import Testing enum SoftServe { case vanilla, chocolate, pineapple }
-
0:16 - Complex types
import Testing struct SoftServe { let flavor: Flavor let container: Container let toppings: [Topping] } @Test(arguments: [ SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream]) ]) func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
-
0:17 - Conforming to CustomTestStringConvertible
import Testing struct SoftServe: CustomTestStringConvertible { let flavor: Flavor let container: Container let toppings: [Topping] var testDescription: String { "\(flavor) in a \(container)" } } @Test(arguments: [ SoftServe(flavor: .vanilla, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .chocolate, container: .cone, toppings: [.sprinkles]), SoftServe(flavor: .pineapple, container: .cup, toppings: [.whippedCream]) ]) func softServeFlavors(_ softServe: SoftServe) { /*...*/ }
-
0:18 - An enumeration with a computed property
extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio var containsNuts: Bool { switch self { case .rockyRoad, .pistachio: return true default: return false } } } }
-
0:19 - A test function for a specific case of an enumeration
import Testing @Test func doesVanillaContainNuts() throws { try #require(!IceCream.Flavor.vanilla.containsNuts) }
-
0:20 - Separate test functions for all cases of an enumeration
import Testing @Test func doesVanillaContainNuts() throws { try #require(!IceCream.Flavor.vanilla.containsNuts) } @Test func doesChocolateContainNuts() throws { try #require(!IceCream.Flavor.chocolate.containsNuts) } @Test func doesStrawberryContainNuts() throws { try #require(!IceCream.Flavor.strawberry.containsNuts) } @Test func doesMintChipContainNuts() throws { try #require(!IceCream.Flavor.mintChip.containsNuts) } @Test func doesRockyRoadContainNuts() throws { try #require(!IceCream.Flavor.rockyRoad.containsNuts) }
-
0:21 - Parameterizing a test with a for loop (not recommended)
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test func doesNotContainNuts() throws { for flavor in [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip] { try #require(!flavor.containsNuts) } }
-
0:22 - Swift testing parameterized tests
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip]) func doesNotContainNuts(flavor: IceCream.Flavor) throws { try #require(!flavor.containsNuts) }
-
0:23 - 100% test coverage
import Testing extension IceCream { enum Flavor { case vanilla, chocolate, strawberry, mintChip, rockyRoad, pistachio } } @Test(arguments: [IceCream.Flavor.vanilla, .chocolate, .strawberry, .mintChip]) func doesNotContainNuts(flavor: IceCream.Flavor) throws { try #require(!flavor.containsNuts) } @Test(arguments: [IceCream.Flavor.rockyRoad, .pistachio]) func containNuts(flavor: IceCream.Flavor) { #expect(flavor.containsNuts) }
-
0:24 - A parameterized test with one argument
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } @Test(arguments: Ingredient.allCases) func cook(_ ingredient: Ingredient) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) }
-
0:26 - Adding a second argument to a parameterized test
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } enum Dish: CaseIterable { case onigiri, fries, salad, omelette } @Test(arguments: Ingredient.allCases, Dish.allCases) func cook(_ ingredient: Ingredient, into dish: Dish) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) try #require(result == dish) }
-
0:28 - Using zip() on arguments
import Testing enum Ingredient: CaseIterable { case rice, potato, lettuce, egg } enum Dish: CaseIterable { case onigiri, fries, salad, omelette } @Test(arguments: zip(Ingredient.allCases, Dish.allCases)) func cook(_ ingredient: Ingredient, into dish: Dish) async throws { #expect(ingredient.isFresh) let result = try cook(ingredient) try #require(result.isDelicious) try #require(result == dish) }
-
0:29 - Suites
@Suite("Various desserts") struct DessertTests { @Test func applePieCrustLayers() { /* ... */ } @Test func lavaCakeBakingTime() { /* ... */ } @Test func eggWaffleFlavors() { /* ... */ } @Test func cheesecakeBakingStrategy() { /* ... */ } @Test func mangoSagoToppings() { /* ... */ } @Test func bananaSplitMinimumScoop() { /* ... */ } }
-
0:30 - Nested suites
import Testing @Suite("Various desserts") struct DessertTests { @Suite struct WarmDesserts { @Test func applePieCrustLayers() { /* ... */ } @Test func lavaCakeBakingTime() { /* ... */ } @Test func eggWaffleFlavors() { /* ... */ } } @Suite struct ColdDesserts { @Test func cheesecakeBakingStrategy() { /* ... */ } @Test func mangoSagoToppings() { /* ... */ } @Test func bananaSplitMinimumScoop() { /* ... */ } } }
-
0:31 - Separate suites
@Suite struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:32 - Separate suites
@Suite struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:35 - Using a tag
import Testing extension Tag { @Tag static var caffeinated: Self } @Suite(.tags(.caffeinated)) struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test(.tags(.caffeinated)) func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:36 - Declare and use a second tag
import Testing extension Tag { @Tag static var caffeinated: Self @Tag static var chocolatey: Self } @Suite(.tags(.caffeinated)) struct DrinkTests { @Test func espressoExtractionTime() { /* ... */ } @Test func greenTeaBrewTime() { /* ... */ } @Test(.tags(.chocolatey)) func mochaIngredientProportion() { /* ... */ } } @Suite struct DessertTests { @Test(.tags(.caffeinated, .chocolatey)) func espressoBrownieTexture() { /* ... */ } @Test func bungeoppangFilling() { /* ... */ } @Test func fruitMochiFlavors() { /* ... */ } }
-
0:37 - Two tests with an unintended data dependency (not recommended)
import Testing // ❌ This code is not concurrency-safe. var cupcake: Cupcake? = nil @Test func bakeCupcake() async { cupcake = await Cupcake.bake(toppedWith: .frosting) // ... } @Test func eatCupcake() async { await eat(cupcake!) // ... }
-
0:38 - Serialized trait
import Testing @Suite("Cupcake tests", .serialized) struct CupcakeTests { var cupcake: Cupcake? @Test func mixingIngredients() { /* ... */ } @Test func baking() { /* ... */ } @Test func decorating() { /* ... */ } @Test func eating() { /* ... */ } }
-
0:39 - Serialized trait with nested suites
import Testing @Suite("Cupcake tests", .serialized) struct CupcakeTests { var cupcake: Cupcake? @Suite("Mini birthday cupcake tests") struct MiniBirthdayCupcakeTests { // ... } @Test(arguments: [...]) func mixing(ingredient: Food) { /* ... */ } @Test func baking() { /* ... */ } @Test func decorating() { /* ... */ } @Test func eating() { /* ... */ } }
-
0:40 - Using async/await in a test
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await eat(cookies, with: .milk) }
-
0:41 - Using a function with a completion handler in a test (not recommended)
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) // ❌ This code will run after the test function returns. eat(cookies, with: .milk) { result, error in #expect(result != nil) } }
-
0:42 - Replacing a completion handler with an asynchronous function call
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await eat(cookies, with: .milk) }
-
0:43 - Using withCheckedThrowingContinuation
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await withCheckedThrowingContinuation { continuation in eat(cookies, with: .milk) { result, error in if let result { continuation.resume(returning: result) } else { continuation.resume(throwing: error) } } } }
-
0:44 - Callback that invokes more than once (not recommended)
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) // ❌ This code is not concurrency-safe. var cookiesEaten = 0 try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) cookiesEaten += 1 } #expect(cookiesEaten == 10) }
-
0:45 - Confirmations on callbacks that invoke more than once
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await confirmation("Ate cookies", expectedCount: 10) { ateCookie in try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) ateCookie() } } }
-
0:46 - Confirmation that occurs 0 times
import Testing @Test func bakeCookies() async throws { let cookies = await Cookie.bake(count: 10) try await confirmation("Ate cookies", expectedCount: 0) { ateCookie in try await eat(cookies, with: .milk) { cookie, crumbs in #expect(!crumbs.in(.milk)) ateCookie() } } }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。