![見出し画像](https://assets.st-note.com/production/uploads/images/29118454/rectangle_large_type_2_c81cbcfeca62b410b27ccee7a0eee7b4.png?width=1200)
Write tests to failをみた #WWDC20
setUp
Xcode 11.4からsetUpWithErrorという新しいsetUp関数が導入されエラーをthrowすることができるようになった。これはテストを実行する前の初期設定で問題が見つかった時に気付けるようにする。
class RecipesTests: XCTestCase {
let app = FrutaApp()
override func setUpWithError() throws {
continueAfterFailure = false
app.launchArguments.append("-recipes-tests")
app.launch()
}
}
こちらのコードでは問題が見つかった時にすぐ失敗するようにcontinueAfterFailureをfalseに設定している。これで複数のエラーを探し回るのではなく最初のエラーを素早く発見することができる。
このテストでは複数のメニューを経由して到達する画面をテストする際にlaunchArcumentsに--recipes-testsを追加して実行している。これはテストの速度を向上させるだけではなく、テストする画面以外の問題を避けることができる。
Action
それぞれのテストには目標があるべき。その目標をテスト名に反映させるべき。テスト名が明確なおかげで結果から何を検証しているのか簡単にみることができる。ラベルのテキストはよく変更される。そこでラベルの文字列にenumを利用している。そうすればUIが変更された際にテストも簡単に更新することができる。
もう一つのミスを最小限に抑える方法として複数のテストが同じコードパスを使用できるように共通のコードをヘルパー関数に組み込むこと。
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()
}
}
さらに採用したテクニックはアプリのドメインをモデル化して、そのドメインを中心にテストを設計すること。
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
}
}
この例ではFrutaAppにスムジーリストをリクエストすることができ、レシピの選択するようなアクションをリスト上で行うことができ、レシピUIを返す。このように共有コードをオブジェクト指向的にした。こうすることでアプリの考え方にマッピングしたテストからの呼び出しを行うことができる。読みやすさのためにオブジェクト指向の環境をシミュレートすることができる。長年に渡って共有テストのコードは大きくなってきたので、共有フレームワークを作成した。特に複数アプリケーション間ででコードを共有している場合には、テストコードを共有するためにSwift Packageを利用することを検討することもできる。このセクションをようやくすると、テストしていることに焦点を当てるために、特定の目標のためにテストを設計する。
Assertion
上部のエラーメッセージは何が何だか分からないので、人間が読みやすいようなメッセージを用意する。
これらをのassertion関数を利用する
非同期のテストではすぐに要素が見つからないかもしれない。そこでwaitForExistingでタイムアウトを設定する。
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)
}
こうすることで5秒待って要素を見つけたことがわかる
もう一つおすすめのテクニックはOptionalのunwrapに関して。
func countFavorites(favorites: [String]?) -> Int{
let favs = favorites!
return favs.count
}
このコードを実行するとこのようなエラーとなる。
このような状況を回避するにはいくつか方法がある。
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”)
4つ目のオプションはXCTest.frameworkに含まれているXCTUnwrapを使用すること。これはguard letを単純化した物でテストでnilに遭遇した場合にエラーを投げる。
XCTUnwrapを利用するとResultバンドルに自動生成されたメッセージに加えて、呼び出しからのコメントが表示される。これの良い点はクラッシュする代わりに潔く失敗することでtearDownメソッドが呼び出されること。
throws
共有コードは多くのテストから実行されているので共有コードからはassertではなくthrowすることにしている。これらのテストの中にはネガティブなテストケースもあった。例えば、非表示になっているはずのテストをしたり、テストの為にエラーダイアログを表示すると言ったこともある。このような確認のために、以前は余計な成分が表示されたテストをしていたかもしれない。ここでは成分が表示されなくなったことを確認している。
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 {
return "\(ingredient) does not exist in the Ingredients View."
}
}
共通テストの中でassertを実行するのではなくCustomStringConvertibleを適合したErrorをthrowする。
そうすることでエラーが発生した時にメッセージが分かりやすい。このようにXCTContext.runActivityを使用して名前を提供するようにしている。更に、これを利用すると良いこととして、ファイル、画像、データなどをXCTContextのattachmentに追加すると結果にも表示され、CI上などで失敗の理由を探す時にはとても役に立つ。ローカルでテスト結果をトリアージする時には、XCTIssueを活用してバックトレースを参照すると良い。XCTIssueに関してはTriage Test Failure With XCTIssueという関連セッションを参考。
テストが全て実行される必要がないことがある。そういう時にはXCTSkip, XCTSkipUnless, XCTSkipIfを利用しメッセージを追加して実行していないことを明示することができる。主な用途はテストが実行されているプラットフォームに関係のないテストをスキップすること。
これを利用することでテストされていない箇所を明確にすることもできる。
まとめ
本当は昨日の早い段階で見てたんですがまとめに時間がかかってしまいました。テストについてのテクニックと最新機能の活用方法、関連ビデオの2つの内容にも触れられており良いテストに関するセッションだったなと思います。