post Image
【まとめ】Engineering for Testability (後半)【WWDC 2017】

はじめに

この記事では、WWDC 2017でのセッション Engineering for Testabilityの後半の内容をまとめます。
前半を先に読んでいただけると良いと思いますが、一応前半を読まなくても後半を理解できる内容になっていると思います。
このセッションでは、テストがしやすいコードの書き方や、保守・スケールしやすいテストコードの書き方などが紹介されました。
また、テストコードの大切さについても話されています。
図はAppleが公開しているスライドから引用しました。

読むのがめんどくさい方へ

結構長いので、すべて読むのがめんどくさい方はこれだけでも覚えていってください。

Treat your test code with the same amount of care as your app code
アプリのコードと同じくらいテストコードに気を配ろう

Code reviews for test code, not code reviews with test code
テストコードと一緒にレビューするのではなく、テストコード自体をレビューしよう

つまり、

スクリーンショット 2017-06-20 19.48.57.png

ではなく、

スクリーンショット 2017-06-20 19.49.13.png

である。

まとめ

Scalable Test Code 〜スケールしやすいテストを書く〜

テストのピラミッド

UI TestとUnit Testの割合は次のピラミッドのようになるようにバランスを保つと良い。

スクリーンショット 2017-06-20 20.20.56.png

保守・スケールしやすいテストを書く方法

  • クエリを抽象化する
  • 一連の処理を行うオブジェクトやユーティリティ関数を利用する
  • キーボードショートカットを簡単に使用する

クエリの抽象化

次のような一連のUI Testは、

app.buttons["blue"].tap()
app.buttons["red"].tap()
app.buttons["yellow"].tap()
app.buttons["green"].tap()
app.buttons["purple"].tap()
app.buttons["orange"].tap()
app.buttons["pink"].tap()

次のようにクエリの一部を変数で保持し、複雑なクエリはラップします。

func tapButton(_ color: String) {
    app.buttons[color].tap()
}

let colors = ["blue", "red", "yellow", "green", "purple", "orange", "pink"]
for color in colors {
    tapButton(color)
}

オブジェクトやユーティリティ関数の定義

次のような一連のUI Testは、ゲームの設定画面を開き、難易度を初心者に設定し、サウンドをOFFにして最初の画面に戻る動作をテストしています。
しかし、このテストコードに馴染みのない人から見ると非常に読みづらいテストコードとなってしまっています。

func testGameWithDifficultyBeginnerAndSoundOff() {
    app.navigationBars["Game.GameView"].buttons["Settings"].tap()
    app.buttons["Difficulty"].tap()
    app.buttons["beginner"].tap()
    app.navigationBars.buttons["Back"].tap()
    app.buttons["Sound"].tap()
    app.buttons["off"].tap()
    app.navigationBars.buttons["Back"].tap()
    app.navigationBars.buttons["Back"].tap()

    // test code
}

そこで、次のように難易度設定とサウンド設定を行うテストをラップします。
enumで管理するようにしておくと良いでしょう。

enum Difficulty {
    case beginner
    case intermediate
    case veteran
}
enum Sound {
    case on
    case off
}
func setDifficulty(_ difficulty: Difficulty) {
    app.buttons["Difficulty"].tap()
    app.buttons[difficulty.rawValue].tap()
    app.navigationBars.buttons["Back"].tap()
}
func setSound(_ sound: Sound) {
    app.buttons["Sound"].tap()
    app.buttons[sound.rawValue].tap()
    app.navigationBars.buttons["Back"].tap()
}

これで次のようにテストを書き直すことができます。
しかし、まだ分かりにくい部分があります。

func testGameWithDifficultyBeginnerAndSoundOff() {
    app.navigationBars["Game.GameView"].buttons["Settings"].tap()
    setDifficulty(.beginner)
    setSound(.off)
    app.navigationBars.buttons["Back"].tap()

    // test code
}

そこで、先ほどのenumなどを持った次のようなクラスを定義します。

class GameApp: XCUIApplication {
    enum Difficulty { /* cases */ }
    enum Sound { /* cases */ }
    func setDifficulty(_ difficulty: Difficulty) { /* code */ }
    func setSound(_ sound: Sound) { /* code */ }

    func configureSettings(difficulty: Difficulty, sound: Sound) {
        app.navigationBars["Game.GameView"].buttons["Settings"].tap()
        setDifficulty(difficulty)
        setSound(sound)
        app.navigationBars.buttons["Back"].tap()
    }
}

上記のクラスに持たせたconfigureSetting(difficulty:sound:)メソッドを利用すると、次のような非常に分かりやすいテストコードを書くことができます。
必要であれば、Xcode9から利用できるXCTContext.runActivity(named name: String, block: (XCTActivity))でテストレポートを見やすくするのも良いでしょう(参考)。

func testGameWithDifficultyBeginnerAndSoundOff() {

    GameApp().configureSettings(difficulty: .beginner, sound: .off)

    // test code

}

キーボードショートカット

これはmacOSアプリでの話になりますが、メニューバーにある機能を使用するとき、以前までは次のようにUI Testを書いていました。

let menuBarsQuery = app.menuBars
menuBarsQuery.menuBarItems["Format"].click()
menuBarsQuery.menuItems["Font"].click()
menuBarsQuery.menuItems["Show Colors"].click()

ですが、もしその機能にショートカットキーが割り当てられているなら、次のように簡単にその機能を呼び出すテストを書くことができます。

app.typeKey("c", modifierFlags:[.command, .shift]) 

さいごに

テストの書き方と大切さが分かるセッションだったと思います。
iOSアプリにテストを導入するきっかけになれば嬉しいです。
最後に、記事の内容に誤りなどがございましたら、気軽に、優しく、コメントしていただけると嬉しいです。
それでは、最後までお読みいただき、ありがとうございました!


『 Swift 』Article List