見出し画像

CSVエディタ開発日記: XCUITestの導入

こんにちは、アプリ開発者の安藤ひつじです。今日は僕が開発しているMacアプリ『CLYR CSVEditor』についての開発日記です。前回、Viewが更新されないバグの話を書きましたが、この記事はその続きになります。


CLYR CSVEditorとは

macOS向けに開発しているCSVファイルを編集するためのアプリケーションです。以下はApp Storeに載せている紹介文の一部です。

美しさと機能性を有するCSVエディタ。

CLYR CSVEditorはCSVファイルを編集するために作られたアプリケーションです。

ボタンによる行・列の追加、削除といった直感的でわかりやすいUI、行数や列数の表示はもちろん、セルのテキストや値の文字数、桁数の表示といった、ちょっと便利なアプリデザイン。

CLYR CSVEditorは、シンプルかつ機能的で誰でも使いやすいCSVエディタを目指しています。

CSVファイルを編集できるアプリの代表格としてはExcelが挙げられますが、Excelのちょっとお節介な機能(ゼロ落ち、指数表記など)のせいでCSVデータが壊れてしまったという経験はありませんか? CLYR CSVEditorは、こういった悩みからユーザを解放し、CSVの扱いに特化した素敵な体験を提供することを目的に開発しています。

XCUITestの導入

開発しているCSV Editorアプリで、セルを複数選択して値を一括削除したとき、Viewが更新されないバグが発生しました。Viewの更新し忘れが直接的な(プログラム的な)原因でしたが、そもそもの問題はUIテストが自動化されておらず、デグレに気付けなかった点にあると考えています。
ということで、XCUITestを使ってUIテストを自動化したいと思います。

検索フィールドを開いた後、escキー押下で閉じるかテスト

まずは簡単なテストを書いてみました。以下は検索フィールドを開いた後、escキー押下で閉じることができるかテストしています。

func testSearchViewShowAndHide() {
    let app = XCUIApplication()
    app.launch()

    let findButton = app.buttons["findButton"]
    // ボタンが存在するか
    XCTAssertTrue(findButton.exists)
    // ボタンが押せるか
    XCTAssertTrue(findButton.isHittable)

    // 検索ボタン押下で検索フィールドが表示されるか
    let searchField = app.textFields["searchField"]
    XCTAssertFalse(searchField.isHittable)
    findButton.click()
    XCTAssertTrue(searchField.isHittable)

    // escキー押下で検索フィールドが閉じられるか
    Thread.sleep(forTimeInterval: 0.5)
    app.typeKey(.escape, modifierFlags: [])
    XCTAssertFalse(searchField.isHittable)
}

各要素のAccessibility IDは以下です。

// 検索ボタン(虫眼鏡アイコン)
findButton.setAccessibilityIdentifier("findButton")

// 検索フィールド
searchField.setAccessibilityIdentifier("searchField")

このテストを実行してみると正しく動作しました(Test Succeeded)。

画像3

画像2

画像3


existsとisHittableの違いを調べてみたところ以下の違いがあるようです。クリックできるかどうかはisHittableで判定すれば良さそうです。

exists
・要素が存在するか判定
・画面外にあるときや他の要素によって隠れている場合もtrue
・view.isHidden = trueのときはfalse
・view.isEnabled = falseのときはtrue
isHittable
・要素が押せるか判定
・画面外や他の要素によって隠れている場合はfalse
・view.isHidden = trueのときもfalse
・view.isEnabled = falseのときもfalse

テストコード中にThread.sleepを挟んでいるのは、アニメーションが完了するのを待つためです。

列を追加できるかテスト

次に列を追加するテストを書いてみました。

func testInsertColumnAfter() {
    let app = XCUIApplication()
    app.launch()

    // 1行1列目のセルに"abc"を入力
    app.textFields["viewCellField0_0"].typeText("abc")

    for col in 0..<2 {
        // ヘッダにマウスカーソルをホバー
        let headerViewCellField = app.textFields["headerViewCellField\(col)"]
        XCTAssertTrue(headerViewCellField.isHittable)
        headerViewCellField.hover()

        // ボタンが表示されるか
        let insertColumnAfterButton = app.buttons["insertColumnAfterButton\(col)"]
        XCTAssertTrue(insertColumnAfterButton.isHittable)

        // ボタン押下で列が挿入されるか
        XCTAssertFalse(app.textFields["viewCellField0_\(col + 1)"].exists)
        insertColumnAfterButton.click()
        XCTAssertTrue(app.textFields["viewCellField0_\(col + 1)"].isHittable)
    }

    XCTAssertEqual("abc", app.textFields["viewCellField0_0"].value as! String)
}

func testInsertColumnBefore() {
    let app = XCUIApplication()
    app.launch()

    app.textFields["viewCellField0_0"].typeText("abc")

    for col in 0..<2 {
        // ヘッダにマウスカーソルをホバー
        let headerViewCellField = app.textFields["headerViewCellField\(col)"]
        XCTAssertTrue(headerViewCellField.isHittable)
        headerViewCellField.hover()

        // ボタンが表示されるか
        let insertColumnBeforeButton = app.buttons["insertColumnBeforeButton\(col)"]
        XCTAssertTrue(insertColumnBeforeButton.isHittable)

        // ボタン押下で列が挿入されるか
        XCTAssertFalse(app.textFields["viewCellField0_\(col + 1)"].exists)
        insertColumnBeforeButton.click()
        XCTAssertTrue(app.textFields["viewCellField0_\(col + 1)"].isHittable)
    }

    XCTAssertEqual("abc", app.textFields["viewCellField0_2"].value as! String)
}

このCSVEditorはセルを動的に生成するため、Accessibility IDも動的にセットしています。幸いにも行と列のインデックスでセルをユニークに特定できるので、Accessibility IDは以下のように決めることができました。

cell.setAccessibilityIdentifier("viewCellField\(rowIndex)_\(columnIndex)")

同様にヘッダーも動的に生成するため、Accessibility IDは以下のように列インデックスを使ってユニークに決定しています。

// ヘッダ名フィールド
textField.setAccessibilityIdentifier("headerViewCellField\(columnIndex)")
// 後ろに列を追加するボタン
insertAfterButton.setAccessibilityIdentifier("insertColumnAfterButton\(columnIndex)")
// 前に列を追加するボタン
insertBeforeButton.setAccessibilityIdentifier("insertColumnBeforeButton\(columnIndex)")

以下は各要素とAccessibility IDの簡易的な関連図です。

画像6

CLYR CSVEditorはヘッダーにマウスカーソルを重ねると、列挿入ボタンが表示される仕様になっています。ということで、マウスオーバーしてボタンが表示されるか、そのボタンを押して列を追加できるか後方挿入と前方挿入それぞれ2回クリックしてテストしています。

画像5

画像6

typeTextメソッドでテキストフィールドに値を入力できるので、1行1列目のセルに"abc"と入力し、このセルの列挿入後の位置と比較することで、正しく動作しているか判定しています。

まとめ

開発しているCSV EditorアプリにXCUITestを導入してみました。まずは簡単なUI操作ならば再現できることがわかりました。ただ、課題もけっこうあると思いました。例えば、

1. TextFieldやButtonならAccessibility IDをキーに要素を取得できたのですが、単純にNSViewを継承したクラスだと取得できませんでした。NSControlを継承していないとダメなのでしょうか
2. ドラッグ操作が再現できていません。press(forDuration:,thenDragTo:)メソッドがそれっぽいですが、うまくいきません
3. 編集中のファイルがあると、テスト起動したときにうまくいかないケースがあります。UIテストをする前に、ファイルはすべて閉じておき、新規作成したファイルが一つある状態にしておく必要がありました

などが挙げられます。また、テストシナリオをプログラミングするのもけっこう大変です。例えば、テキストフィールドに値を入力する場合、まずはその要素をクリックし、フォーカスを当ててからでないと入力できないなど。なかなかコツがいるなと。
実際は工数見合いで重要なやつからテストしていく必要がありますね。

アプリの宣伝

CLYR CSVEditorはApp Storeでリリース中です。興味がある方はインストールしてみてください。アプリ起動時に表示されるサブスクリプション購入画面で購入ボタンをクリックしていただくと、最初の2週間は無料でトライアルいただけます。トライアル期間終了後に自動的に課金されますので、もし使ってみて気に入らなければ、トライアル期間中にキャンセルしていただいて構いません。でも、長く使っていただけると嬉しいです!




この記事が気に入ったらサポートをしてみませんか?