ストリーミングはほとんどのブラウザと
Developerアプリで視聴できます。
-
不具合検出テストを書く
不具合のため計画:最も手強いバグでさえも発見し、診断するのに役立つ有効なテストをデザインしましょう。最良のコードにおいてでも、隠れた問題を発見できるように、XCTestを使った自動テストの改善方法についてお伝えします。問題のトリアージを容易にする不具合検出テストの準備方法、インターフェースの問題を解決し、素早く修正できる方法もお伝えします。 このセッションを有効に活用するためには、XCTestフレームワーク内でUIテストを書くことに慣れていることが望ましいです。 テストツールの詳細については、“The suite life of testing”に進んでください。
リソース
関連ビデオ
WWDC22
WWDC20
-
ダウンロード
“ようこそ WWDCへ” こんにちは WWDCへようこそ “不具合検出テストを書く” “不具合検出テストを書く”へようこそ ケリー・キーナンです このセッションでは― 私が長年 XcodeでUIと統合テストを 書いてきて学んだ教訓をいくつか紹介します UIテストが主ですが― 多くの教訓は単体テストにも応用できます テストを書いたのが コードを書く前か書いた後かにかかわらず― 一番の原動力は テストをグリーンにすることです テストが成功すれば このアイコンが現れ― 製品が出荷できるからです でも今年は “不具合検出テストを書く”をモットーにします いいテストはバグを発見できます 不具合に備えましょう テストというのは 書くのは1度ですが トリアージは何度も行われます テストはバグが見つかると失敗します それがテストの目的です 私のテストは すべて連続した統合システムで走らせます 不具合をトリアージするためのツールは テスト結果バンドルです 今回は テスト結果バンドルだけで より楽にトリアージする方法を紹介します テストをより堅牢なものにし― テストのデバッグでなく 製品の不具合のトリアージに集中するためです テストのテンプレートの流れは― セットアップ テスト テアダウンです テストのセクションは アクションとアサーションに分解できます このセッションでは これをテーマとします ではテストのセットアップから始めましょう 私はここで必要な前提条件を宣言し― テスト前に 環境とアプリケーションの状態を設定します Xcode 11.4では 新しいsetUp関数を導入しました setUpWithErrorです スローして― セットアップ中に投げられたエラーを キャッチまたはパスできるようにします 既存のsetUpメソッドを現代化し― エラー管理に活用すると便利でした setUpWithErrorメソッドを使い― 実行前に テストに必要な初期状態を設定します 過去のテストで アプリケーションの状態やテストで使うデータが 変更されているかもしれないからです
この例では continueAfterFailureを“false”に設定し― 問題があれば ただちにテストが失敗するようにします 複数のエラーを待たずに 最初のエラーを早く見つけやすくなります またこのクラスでは 各テストで アプリケーションを起動するようにします テクニックの1つとして― launchArgumentsと環境変数を活用し アプリケーション内で状態を迅速に設定します すべての状態に使うべきではありません でも必要なケースもあるでしょう 例えばテスト中に 2ファクタ認証をバイパスする場合などです この例では メニュータブをバイパスして― レシピタブから始めるために使っています こうした小さな変更で 不要な作業が削られ テストスピードが改善します でも何より レシピタブのテスト結果を 見たいという時に― メニュータブの不具合を トリアージせずに済みます まとめると setUpWithError() throwsでエラー処理を改善 クラス内の各テストで 共通セットアップタスクを実行します 例えばアプリケーションの起動です launchArgumentsで アプリケーションとやり取りして状態を設定 そして製品変化を取り入れ 状態を迅速に設定し テストに集中します 次のステップはテストの実行です テストではアクションを実行し― 完了をアサートすることに集中すべきです ではアクションを より楽にトリアージする方法を見ましょう まず考えるのは― 各テストに目標を持たせることです 目標はテストのタイトルに反映すべきです この例では 材料リストの正確さをテストしています テストが実行する唯一のアクションは Berry Blueレシピを選択することです アクションを最小限にすることで 後で不具合のトリアージを楽にします この列をタップするとレシピが現れ― アクションの結果として 材料リストを確認できます 結果バンドルでは― テスト名のおかげで 何を確認するテストかすぐ分かります 名前と言えば― UI要素のラベルは よく変わることを経験から学びました そこで予防策として すべての文字列値に列挙型を使います そうすればUIが変わった時― 簡単にテストをアップデートして 変更に対応できます UI変更のテストをアップデートする時間を 節約するだけでなく― 気づきにくいスペルミスのせいで テストが失敗する回数を最小限にできます 文字列を列挙型に集めたように ミスを最小限にする他の方法として― ヘルパー関数に 共通コードを組み入れることです 複数のテストで同じコードパスが使えます この例では 複数のテストが スムージーリストにアクセスし― レシピを選択します 共通テストパスを引き抜くことにより コードを複製せずに― テストエラーを減らすため パスを堅牢にすることに注力できます 他にもテクニックとして アプリケーションの ドメインをモデル化したものを軸に― テスト言語を設計する方法があります そうすれば テストに アプリケーションの言語が反映されます この例では FrutaAppにスムージーリストを要求し― スムージーリストでアクションを実行できます 例えばレシピを選択すると― レシピUI要素が返されます アプリケーションと 下位レベルの要素を把握するために作成した― FrutaUIElementクラスに基づいています こうすることで共通コードを オブジェクト指向的にしました テストは関数的で 要素やクエリーに基づいていますが― 可読性のために オブジェクト指向の環境をシミュレートできます これにより 私が考えるアプリケーションを 一連のサブビューとして マッピングするコールをテストで行うことが 可能になります このモデル化の結果― 各要素で より少ないヒエラルキーの作業になります そしてクエリーを 要素のサブ要素だけに集中させることができます これまでに私たちが共有したテストコードは かなりの量になりました そこで製品コードと同じように扱い― テストの共有フレームワークを作りました Swiftパッケージを使って テストコードを共有するのもいいでしょう 複数のアプリケーションで 共有する場合は特にです まとめると 明確な目標を定めてテストを設計します 列挙型を使い ヘルパー関数に共通コードを組み入れ― UI変更の対応を簡略化します UIヒエラルキーを反映するため テストでオブジェクトをモデル化します フレームワークやSwiftパッケージを使って― コードを共有します 次の話は私の大好きなテストアサーション テストの核となる部分です テストアサーションとエラー処理に関して 不具合をトリアージしやすくする上で― 私が学んだ教訓を紹介します 1つ大いに役立ったのは― XCTAssert関数で 任意のメッセージを使うことです デスクでテストのトリアージをするなら メッセージを省いても構いませんが― 結果バンドルしかない時は いろいろと文脈が足りません この例では 3が2とイコールでないことは分かりますが 何が2なのか? メッセージを加えれば文脈を加えられます 人は多くの時間 アサーションメッセージを読んでいます 私も頻繁に読みます だから自分に なぜこの表現が失敗したかヒントを残します でも時には 自動システムによって読まれることもあります その場合は メッセージを適度に明確にしておきたいです そこで日付やタイムスタンプや 固有のファイルパスなどを省きます 同じ理由で失敗している複数のテストを 認識するのに― メッセージを使うためです さらに 正しいアサーションを使っているか― 確認を試みます こうすることで失敗した時に出る 自動メッセージの関連性を高めます Xcode 12ではXCTIssueを追加しました 不具合を報告する新しい下位レベルの方法です 詳しい情報は― “Triage test failures with XCTIssue”を ご覧ください アサーションの落とし穴の1つは 非同期イベントです 非同期イベントのトリアージは時に苦労します この例では レシピボタンをタップしますが― コードが何をするかによっては 時間がかかります すぐにレシピ要素を返しても― まだ存在しないかもしれません 過去には スリープを使って テストに内臓タイムを与えたこともあります でもテストにやらせる必要はないし― 結果が出るのが遅れます XCTestは内臓リトライがありますが アプリケーションのコードによっては 十分ではないかもしれません だから私は waitForExistence(timeout)をお勧めします ポーリングができるので― タイムアウトより早く期待値が“true”なら その分 待たずに済んだことになります そして設計した環境内で テストが 確定的にパスまたは失敗しやすくなります 結果バンドルを見ると材料ビューを見つけるのに 5分待ったことが分かります もう1つの推奨は Optional型のアンラップです この例では パスされる文字列内の お気に入りのカウントを返したいのですが 私はOptional型をアンラップしませんでした ローカルでコードを走らせる時― これはクラッシュに終わり テストは失敗します ランチの間に走らせて 戻っても終わっていなかったら― とても悲しいです 連続した統合環境で起きると― テストは失敗し 結果バンドルに “不適切な信号でテストがクラッシュ”と出ます Optional型をアンラップすれば こうなることを簡単に避けられます Swiftのメソッドを使い Optional型をアンラップします 例えば“if let”で アンラップされた値をifブロックで使います アンラップされた変数を後で使いたいなら “guard let”を使います “nil”に遭遇すると guardブロックで提供されるエラーを― スローできます 3つ目の選択肢は nil coalescing演算子を使う方法です “nil”に遭遇すると デフォルト値を提供することができます この例では空の配列です 4つ目はXCTUnwrapを使う方法です XCTestフレームワークで提供されています “guard let”の簡略化で― “nil”に遭遇するとエラーをスローします XCTUnwrapを使うと― 結果バンドル内の自動生成メッセージに加えて コールから私のコメントが示されます Optional型をアンラップすることの 最大の利点は― クラッシュするよりも優雅に失敗することで tearDownメソッドが呼び出されることです 優雅な失敗と言えば エラーのスローについて話しましょう 私のテストでは 共有コードから アサートではなくスローするのがルールです 共有コードは多くのテストで実行されており いくつかのテストでは 意図的に ネガティブテストをしているかもしれません 隠したものが表示されないか確認したり テスト目的で エラーダイアログを表示させたりするためです この例では 材料を確認する共有メソッドがあります 以前 追加材料が表示されるバグがあり 修正されたかをテストするとします そこでエラーを投げます エラーには 私はよく descriptionに出てほしい値をパスします CustomStringConvertibleプロトコルの要件です description関数を使えば ローカルでトリアージしない時 結果バンドルに 文脈的に 関連性の高いエラーが示されやすくなります ローカルでトリアージする場合― Xcode 12ではコードで直接 エラーのバックトレースが見られます 共有コードのどこにエラーが隠れているか 悩まずに済みます またRuntime Issues Navigatorでも バックトレースを見つけることができます バックトレースの活用方法について― 詳しくは― “Triage test failures with XCTIssue”を ご覧ください また この結果バンドルには ユーザーが読めるdisclosure groupがあります 当時 私がどんなアクションを取っていたか 文脈を示すためにコードが追加したものです ここではすぐに私がBerry Blueスムージー内で Grapeを探していたと分かりますが これは間違いです こうした道筋を示すため― 私はXCTContext.runActivityを使って 名前を提供します これがブロック内で実行されたアクションと共に 結果バンドルに示されます 結果バンドルに 組織性と文脈を加えるにはいい方法です アクションに沿って読みやすくなります またrunActivityを使って― XCTAttachmentでattachmentを追加できます ファイルや画像やデータなどの添付物が XCTContextかテストケースに追加され― 結果バンドルに出ます 失敗したテストで 追加のロギングを集めるには有効です CIシステムでは特にです 先ほどファイルパスを アサートコメントに追加しないよう言いました ファイルパスとファイルそのものを― 添付して追加すればいいからです 後からトリアージしやすくなります テストは製品不具合をトリアージするのに 必要なデータをすべて集めるのが仕事です そのデータは 後で手に入るか分からないからです 時には実行が不要なテストがあります そういう時は XCTSkipやXCTSkipUnlessや XCTSkipIfを使い― メッセージを追加して 走っていないテストを記録します 主な目的は プラットフォームに 関連のないテストをスキップすることです 私が実際に使ったのは― 新機能のために書きたいテストを スタブアウトする時です どのテストが未実装で どのテストが回帰したか分かるようになります 3つ目は さまざまな理由から 今は解決できないテストがある場合です 不具合のトリアージを続けたくないが― 無効にしてテストを見失いたくない時です XCTSkipを使えば スキップされたテストも結果バンドルに出るので 問題が解決した時 テストを書いたり直したりせずに済みます
まとめると― アサーションメッセージを追加して 関連するXCTAssert関数を使い― 結果バンドルの不具合に文脈を加えます Optional型をアンラップして― ランチ中にテストが クラッシュしないようにします またtearDownメソッドを呼び出させます 非同期イベントやタイミング問題には スリープでなく― waitForExistenceメソッドを使います 他のテストでネガティブテストのエラーを キャッチできるよう アサートはせず― 共有コードからエラーをスローします XCTContext.runActivityと attachmentを使って― 結果バンドルに 文脈とコンテンツを追加します XCTSkipを使って 今のシナリオでは実行したくないテストを― スキップします 最後にテアダウンです テアダウンに関しては― 推奨することが3つだけあります 1つはtearDownWithError() throwsを使って テストを現代化し― 新しいエラー管理を活用します 次にtearDownメソッドを使って― 不具合の分析を含めた追加のロギングを集めます そしてその時 設定中に行った変更から 環境をリセットします 全体をまとめると― セットアップでは 環境を変更し テストに必要な前提条件を確認します テストのアクションでは アプリケーションを モデル化した共有コードを通じて テストしたい必要なアクションを実行します 次にヘルパーメソッドやエラーや テストアサーションを使って― アクションが 正しく完了されることを確認します そして最後にtearDownメソッドで データを集め テスト後の片づけをします これらのテクニックと推奨事項が 皆さんのテストをより堅牢なものにし― トリアージが簡単かつ迅速になること そしてテストをグリーンにし 製品を出荷する手助けになることを祈っています ありがとうございました
-
-
1:58 - Use setUpWithError()
class RecipesTests: XCTestCase { let app = FrutaApp() override func setUpWithError() throws { continueAfterFailure = false app.launchArguments.append("-recipes-tests") app.launch() } }
-
3:09 - Use launch arguments
class RecipesTests: XCTestCase { let app = FrutaApp() override func setUpWithError() throws { continueAfterFailure = false app.launchArguments.append("-recipes-tests") app.launch() } } @State private var selection: Tab = CommandLine.arguments.contains("-recipes-tests") ? .recipes : .menu
-
4:12 - Design tests for a specific goal
func testIngredientsListAccuracy() throws { // Select Berry Blue recipe let recipe = try app.smoothieList().selectRecipe (smoothie: .berryBlue) // Verify ingredients list try recipe.verify(ingredients: SmoothieType.berryBlue.ingredients) }
-
4:56 - Use enums for string values
public enum SmoothieType : String { case berryBlue = "Berry Blue" case carrotChops = "Carrot Chops" case berryBananas = "That's Berry Bananas!" var ingredients : [String] { switch self { case .berryBlue: return ["Orange", "Blueberry", "Avocado"] case .carrotChops: return ["Orange", "Carrot", "Mango"] case .berryBananas: return ["Almond Milk", "Banana", "Strawberry"] } } }
-
5:25 - Factor common code
let recipe = try app.smoothieList().selectRecipe(smoothie: .berryBlue) public class FrutaApp : XCUIApplication { public func smoothieList() throws -> SmoothieList { let element = tables["Smoothie List"] if !element.waitForExistence(timeout: 5) { throw FrutaError.elementDoesNotExist("Smoothie List table") } return SmoothieList(app: self, element: element) } } public class SmoothieList : FrutaUIElement { public func selectRecipe(smoothie: SmoothieType) throws -> Recipe { element.buttons[smoothie.rawValue].tap() return try app.recipe() } }
-
5:49 - Model UI hierarchy in testing code
public class FrutaApp : XCUIApplication { public func smoothieList() throws -> SmoothieList {  } } public class SmoothieList : FrutaUIElement { public func selectRecipe(smoothie: SmoothieType) throws -> Recipe {  } } open class FrutaUIElement { let app: FrutaApp let element: XCUIElement init(app: FrutaApp, element: XCUIElement) { self.app = app self.element = element } }
-
8:17 - Use assertion messages
XCTAssertEqual(count, expectedCount, "\(SmoothieType.berryBlue.rawValue) smoothie is expected to have \(expectedCount) ingredients: \(expectedIngredients), however, there were \(count) found.")
-
9:21 - Asynchronous events
public func selectRecipe(smoothie: SmoothieType) throws -> Recipe { element.buttons[smoothie.rawValue].tap() return try app.recipe() } public func recipe() throws -> Recipe { let element = scrollViews["Ingredients View"] if !element.waitForExistence(timeout: 5) { throw FrutaError.elementDoesNotExist( "Ingredients View scroll view") } return Recipe(app: self, element: element) }
-
10:19 - Unwrapping optionals
func countFavorites(favorites: [String]?) -> Int{ let favs = favorites! return favs.count }
-
10:56 - Unwrapping optionals continued
if let favs = favorites {  } guard let favs = favorites else { /* throw an error */ } let favs = favorites ?? [] let favs = try XCTUnwrap(favorites, "favorites is nil, so there is nothing to count”)
-
12:19 - Throw errors from shared code
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { throw RecipeError.ingredientDoesNotExist(ingredient) } } } } public enum RecipeError : Error, CustomStringConvertible { case ingredientDoesNotExist(String) public var description : String { switch self { case .ingredientDoesNotExist(let ingredient): return "\(ingredient) does not exist in the Ingredients View.)" } } }
-
13:41 - Use XCTContext.runActivity()
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { throw RecipeError.ingredientDoesNotExist(ingredient) } } }
-
14:02 - Add attachments to the result bundle
public func verify(ingredients: [String]) throws { try XCTContext.runActivity(named: "Verifying \(ingredients) exists in the Recipe screen.") { verifyingRecipe in for ingredient in ingredients { if !element.switches[ingredient].waitForExistence(timeout: 5) { let attachment = XCTAttachment(string: element.debugDescription) verifyingRecipe.add(attachment) throw RecipeError.ingredientDoesNotExist(ingredient) } } }
-
14:50 - Use XCTSkip
let debuggingTests = false func testSelectSmoothie() throws { try XCTSkipUnless(debuggingTests == true, "This test is not yet implemented.") }
-
-
特定のトピックをお探しの場合は、上にトピックを入力すると、関連するトピックにすばやく移動できます。
クエリの送信中にエラーが発生しました。インターネット接続を確認して、もう一度お試しください。