WWDC Quick Look 💓 By SwiftGGTeam
Write tests to fail

Write tests to fail

元の動画を見る

ハイライト

Apple が使い方をデモンストレーションsetUpWithError、launch arguments、waitForExistenceXCTUnwrapXCTContext.runActivity、添付ファイル 和XCTSkipUI テストの失敗を問題に変換し、結果バンドルですぐに特定できるようにします。


主要内容

多くのチームがテストを作成するとき、最初の目標はグリーンパスを見ることです。Kelly Keenan は冒頭で角度を変えました。テストの真に価値のある瞬間は、CI で製品の問題が発見されたときであり、開発者が手にしているのは結果バンドルだけです。テストは一度記述され、失敗は何度もチェックされるため、テストコードは失敗に対するコンテキストを事前に準備する必要があります。

このセッションは UI テストと統合テストに焦点を当て、テストをセットアップ、アクション、アサーション、分解に分割します。各ステップは同じ目的を果たします。つまり、失敗した場合、何が起こったのか、どこで起こったのか、なぜ調査に値するのかをレポートで直接説明します。setUpWithError初期状態を明確にする責任を負い、起動引数によってアプリがテストの入り口に入ることができ、テスト メソッド名とドメイン ヘルパーによってアクションがセマンティックになります。

アサーションの部分は診断の品質の中心です。裸のXCTAssertEqual値が異なることのみを伝えることができ、コンテキスト アサーション メッセージによって、どのスムージーに成分が不足しているかがわかります。非同期 UI が発生した場合は、セッションを使用することをお勧めしますwaitForExistence(timeout:)、固定のパラメータを使用する代わりに、テストが制御可能な時間内でターゲット要素をポーリングできるようにします。sleepすべての結果が遅くなります。

共有テスト コードの設計も重要です。session は、共有コードが直接アサートする代わりにエラーをスローすることを明示的に推奨します。このようにして、同じヘルパーをポジティブ テスト、ネガティブ テスト、およびエラー ポップアップ テストで再利用できます。協力するXCTContext.runActivityそしてXCTAttachment、結果バンドルには読み取り可能なステップ、要素 debugDescription、ファイル、またはログが残るため、CI 上の障害がローカルで再発するのを待ってからそれらを見つける必要がなくなります。

詳細

1. セットアップを使用して各テストの開始点を修正します

(01:58) Xcode 11.4 で導入されましたsetUpWithError()エラーがスローされる可能性があります。session これを使用して、各テストの前に初期状態を設定し、継続を閉じ、起動引数を渡し、アプリを開始します。

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

キーポイント:

  • setUpWithError()不明な状態でテストを実行し続けるのではなく、セットアップ フェーズでエラーを XCTest に渡すことができます。
  • continueAfterFailure = false結果バンドル内のノイズを減らすために、最初の問題でテストを停止します。
  • app.launchArguments.append("-recipes-tests")テストインテントをアプリに渡します。
  • アプリ側の読み取りCommandLine.arguments次に、[レシピ] タブに直接移動し、この目標に関係のない [メニュー] タブは避けます。

2. 明確な目標を中心にテスト アクションを行う

(04:12) セッションのサンプル テストでは、ベリー ブルーのレシピを選択し、成分リストを検証することだけが行われます。メソッド名、アクション、アサーションはすべて同じ目標に焦点を当てています。

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)
}

キーポイント:

  • testIngredientsListAccuracyテストの目標を直接記述すると、レポートでテストが何を検証しているかを推測する必要がありません。
  • テストアクションはレシピを選択することしかできず、失敗した場合のトラブルシューティングの範囲は非常に狭いです。
  • verify(ingredients:)アサーションの詳細をヘルパーに追加すると、テスト メソッドはビジネス セマンティクスを保持します。

(04:56) UI ラベルは頻繁に変更されるため、セッションでは文字列を enum に集中することが推奨されます。スペルまたはコピーライティングの変更には 1 回の変更のみが必要です。

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"]
        }
    }
}

キーポイント:

  • rawValueUI に表示されるラベルを保存します。
  • ingredients予期されるデータを同じ型に置くことで、テストでハードコーディングされた配列をあらゆる場所に分散させる必要がなくなります。
  • UI コピーが変更されると、失敗は複数のテストに分散されるのではなく、列挙型に集中します。

3. ドメイン モデルを使用して UI 階層をカプセル化する

(05:25) 複数のテストがスムージー リストに入力され、レシピを選択します。セッションはこのパスを次のように抽出しますFrutaAppSmoothieListそしてRecipeテスト オブジェクトを待って、テスト コードでアプリ ドメイン言語のようなアクションを表現します。

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()
   }
}

キーポイント:

  • app.smoothieList()基になるクエリはヘルパーに含まれており、テスト メソッドはテーブル クエリの詳細を直接操作しません。
  • waitForExistence(timeout: 5)非同期 UI に明確な待機ウィンドウを与えます。
  • 見つかりません"Smoothie List"コンテキスト エラーがスローされると、結果バンドルは実際に欠落している要素を指します。
  • selectRecipe(smoothie:)戻るRecipeにより、後続のアサーションがアプリの UI 階層に沿って継続できるようになります。

4. アサーション、オプションの非同期待機のためにコンテキストを残します。

(08:17) メッセージが人間と自動システムに向けてのものであることを表明します。セッションでは、メッセージは具体的ですが、タイムスタンプや一意のファイル パスを詰め込まないことをお勧めします。これは、自動化システムが同様の障害をグループ化するのを防ぐためです。

XCTAssertEqual(
    count,
    expectedCount,
    "\(SmoothieType.berryBlue.rawValue) smoothie is expected to have \(expectedCount) ingredients: \(expectedIngredients), however, there were \(count) found."
)

キーポイント:

  • countそしてexpectedCount機械が比較できる数値です。
  • メッセージにはスムージーの名前と予想される成分が補足されているため、レポートを読む人はビジネスへの影響を理解できます。
  • メッセージはローカル パスやタイムスタンプを使用して追加されないため、同様の障害を簡単に集計できます。

(09:21) 非同期 UI は依存すべきではありませんsleep。この例では、レシピをクリックした後に成分ビューが表示されるまで待機し、タイムアウト後に明確なエラーをスローします。

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)
}

キーポイント:

  • tap()インターフェイスの変更をトリガーします。
  • waitForExistence(timeout: 5)ターゲット要素をポーリングし、要素が早期に出現した場合、テストは早期に続行されます。
  • タイムアウト パスのスロー"Ingredients View scroll view"、一定の待機後に失敗し続けるよりも優れた位置決め。

(10:56) オプションの直接強制削除によりテストがクラッシュし、信号のみが CI レポートに残ります。セッションは Swift の解凍方法をリストし、具体的に言及しますXCTUnwrap開発者が提供したメッセージは結果バンドルに書き込まれます。

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")

キーポイント:

  • if letラップされていない値をローカルでのみ使用する場合に適しています。
  • guard letnil が使用されたときに定義したエラーをスローするのに適しています。
  • ?? []nil に適切なデフォルト値があるシナリオに適しています。
  • XCTUnwrapnil で XCTest の記録可能な失敗をスローし、カスタムの説明を保持します。

5. 共有コードからエラーをスローし、トラブルシューティング資料を結果バンドルに書き込みます

(12:19) 共有テスト コードは多くのテストで再利用されます。一部のテストはたまたまネガティブ パスを検証するため、セッションはヘルパーが直接アサートする代わりにエラーをスローすることを提案します。

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)
            }
        }
    }
}

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."
        }
    }
}

キーポイント:

  • XCTContext.runActivity結果バンドルに読み取り可能なステップを作成します。
  • アクティビティ名は現在検証されている要素を書き出し、失敗時のアクション コンテキストを保持します。
  • XCTAttachment(string: element.debugDescription)失敗した UI 要素の状態を保存します。
  • RecipeError成し遂げるCustomStringConvertible、エラーの説明は、不足している成分を直接説明できます。

(14:50) 現時点では一部のテストを実行すべきではありません。セッション用XCTSkipファミリ API は、テストの存在を維持しながら、スキップの理由を報告します。

let debuggingTests = false

func testSelectSmoothie() throws {
    try XCTSkipUnless(debuggingTests == true, "This test is not yet implemented.")
}

キーポイント:

  • XCTSkipUnless条件が満たされない場合はテストをスキップします。
  • 結果バンドルにメッセージが表示され、チームはテストが実行されなかった理由を知ることができます。
  • 状態をスキップすることは、テストをコメントアウトするよりも追跡しやすく、後で同じテストに戻って実装や修復を続けることができます。

重要ポイント

  • UI テストの起動ステータス スイッチを追加: 対処方法: 専用の起動引数をキー UI テストに追加して、アプリがテスト中のタブ、ページ、またはモック状態に直接入ることができるようにします。実行する価値がある理由: セッションのレシピ サンプルにより、無関係な [メニュー] タブの失敗が減り、結果バンドルがこのテスト目標に集中できるようになります。開始方法: テスト ターゲットに追加するapp.launchArguments、アプリの起動パスを読み取りますCommandLine.argumentsそして初期タブを切り替えます。

  • 共通 UI パスをテスト ドメイン モデルにカプセル化: 作業内容: アプリ、リスト ページ、詳細ページ、ポップアップ ウィンドウ用に作成します。XCUIApplication/XCUIElementヘルパー。実行する価値がある理由: 複数のテストが同じ UI パスを共有する場合は、それらを一緒に追加しますwaitForExistenceエラーの説明により、不安定な失敗を減らすことができます。開始方法: 最も頻繁に失敗するリスト選択パスから開始し、基になるクエリを次のようなものでラップします。app.smoothieList().selectRecipe(...)方法。

  • 人間が判読できる手がかりを CI 障害レポートに追加します: 対処方法: ビジネス メッセージをコア アサーションに追加し、UI ヘルパーで使用します。XCTContext.runActivityそしてXCTAttachment。実行する価値がある理由: 通常、CI には結果バンドルのみがあり、セッションでは、テストでは製品の障害のトラブルシューティングに必要なデータを積極的に収集する必要があることが強調されます。開始方法: まず、再現が最も難しい UI エラーにアクティビティ名を追加してから、element.debugDescription、ログ、または添付ファイルとしてファイルを送信します。

  • 使用waitForExistence固定待機を置き換えます: 対処方法: UI テストを入れますsleepタイムアウト付きで要素を待機するように変更します。実行する価値がある理由: 待機を修正すると、成功へのパスが遅くなり、ターゲット要素のコンテキストがないと非同期エラーが発生する可能性があります。開始方法: ページ ジャンプとネットワーク ロード後の最初のキー要素をヘルパーにカプセル化し、待機してヘルパーで特定のエラーをスローします。

  • テストをスキップするための明示的なルールを確立します: 何をすべきか: を使用します。XCTSkipXCTSkipIfまたはXCTSkipUnlessプラットフォームの制限、実装されていないテスト、および一時的に修正できないテストを文書化します。実行する価値がある理由: テスト レポートにはスキップされたステータスが表示され、コメントやコンパイル条件で未実行のテストが非表示になりません。開始方法: まず、テスト スイート内のコメントアウトされたプレースホルダー ケースを置き換え、各スキップの理由と回復条件を書き込みます。

関連セッション

コメント

GitHub Issues · utterances