STORES Product Blog

こだわりを持ったお商売を支える「STORES」のテクノロジー部門のメンバーによるブログです。

STORES ブランドアプリ AndroidのE2Eテストの実装 - Robotパターンの活用

こんにちは、naberyo (@error96num) です。今年の4月に STORES へ入社し、 STORES ブランドアプリ のAndroidエンジニアをしています。

前回の記事では、 STORES ブランドアプリ におけるE2Eテスト(End-to-Endテスト)導入の背景とプロセスについて紹介しました。

product.st.inc

本記事では、Android アプリのE2Eテスト実装で、可読性・再利用性・保守性を向上するために採用した Robotパターンの考え方と、Compose UI Testを使った具体的な実装例を紹介します。

Robotパターンとは?

Robotパターンは、テスト対象の画面やフローごとに「ロボット」クラスを定義し、ユーザー操作を模倣するメソッドを集約する設計パターンです。このパターンを採用することで、以下のようなメリットがあります。

  • 可読性: テストケースが「何をテストしているのか」を一目で理解しやすくなる。
  • 再利用性: 各画面やフローのロジックをカプセル化することで、他のテストケースでも再利用できる。
  • 保守性: 特定の画面やフローに変更があった場合でも、基本的には該当するロボットクラスの修正だけで対応できる。

STORES ブランドアプリ の例 - ログインテスト

STORES ブランドアプリ では、次のようなログイン画面でユーザーがアカウントにログインできます。

STORES ブランドアプリ ログイン画面

ログインが成功すると、ユーザーは次のような画面に遷移し、画面上部にログインユーザー名が表示されます。

STORES ブランドアプリ ログイン後の画面

Robotパターンを使わない場合

以下は、Robotパターンを使わず、Compose UI Testのメソッドを直接記述した場合のテストコードです。

class LoginE2ETest {

    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun testLogin() {
        launchApp<MainActivity>()

        // ユーザーのログイン操作をシミュレート
        composeRule.onNodeWithText("メールアドレス")
            .performClick()
            .performTextInput("test_user@example.com")
        composeRule.onNodeWithText("パスワード")
            .performClick()
            .performTextInput("xxxxxxxx")
        composeRule.onNode(hasText("ログイン").and(hasClickAction()))
            .performClick()
            
        // ログイン後の画面を検証
        composeRule.onNodeWithText("テスト ユーザー 様")
            .assertIsDisplayed()
    }
}

この方法では、同じ「ログイン操作」を別のテストケースでも使用する場合、操作ロジックを再度記述しなければなりません。また、コード内の操作内容を直感的に読み取ることが難しく、可読性にも課題があります。

Robotパターンを使った場合

Robotパターンを採用すると、以下のようにテストコードが簡潔になります。

テストコード

class LoginE2ETest {

    @get:Rule
    val composeRule = createComposeRule()

    @Test
    fun testLogin() {
        launchApp<MainActivity>()

        with(LoginScreenRobot(composeRule)) {
            inputEmail("test_user@example.com")
            inputPassword("xxxxxxxx")
            clickLogin()
        }
        
        with(OthersScreenRobot(composeRule)) {
            assertUserNameIsDisplayed(
                lastname = "テスト",
                firstname = "ユーザー",
            )
        }
    }
}

ログイン画面のロボット

Compose UI Testのメソッドを使った詳細な実装については、各画面のロボットクラスに持たせます。

class LoginScreenRobot(private val composeRule: ComposeTestRule) {

    fun inputEmail(email: String) {
        composeRule.onNodeWithText("メールアドレス")
            .performClick()
            .performTextInput(email)
    }

    fun inputPassword(password: String) {
        composeRule.onNodeWithText("パスワード")
            .performClick()
            .performTextInput(password)
    }

    fun clickLogin() {
        composeRule.onNode(hasText("ログイン").and(hasClickAction()))
            .performClick()
    }
}

ログイン後の画面のロボット

class OthersScreenRobot(private val composeRule: ComposeTestRule) {

    fun assertUserNameIsDisplayed(
        lastname: String,
        firstname: String,
    ) {
        composeRule.onNodeWithText("$lastname $firstname 様")
            .assertIsDisplayed()
    }
}

Robotパターンを使う利点

可読性の向上

このようにRobotパターンを取り入れることで、Compose UI Testフレームワークのような「どのようにテストしているか」についての詳細がLoginE2ETestクラスから引き剥がされています。よって、コードが簡潔になり、「何をテストしているか」を把握しやすくなります。

再利用性の向上

LoginScreenRobotに定義した「ログイン操作」のロジックは、他のテストケースでも再利用できます。例えば、ログインしないと利用できない別な機能についてテストを書く場合に、次のようにテスト実行前の処理としてログイン操作を記述できます。

class AnotherE2ETest {

    @get:Rule
    val composeRule = createComposeRule()
    
    @Before
    fun setUp() {
        launchApp<MainActivity>()
        
        with(LoginScreenRobot(composeRule)) {
            inputEmail("test_user@example.com")
            inputPassword("xxxxxxxx")
            clickLogin()
        }
    }

    @Test
    fun testSomething() {        
        // ログインを前提とした機能のテスト
        ....
    }
}

保守性の向上

Robotパターンを採用することで、UIやテキストが変更された場合でも、テストコード全体への影響を最小限に抑えられるというメリットがあります。

例えば、STORES ブランドアプリ のログイン画面で、ログインボタンのテキストが「ログイン」から「サインイン」に変更された場合を考えてみます。この変更により、直接Compose UI Testのコードを記述しているテストケースがあれば、該当箇所をすべて修正しなければなりません。

しかし、Robotパターンを採用している場合には、該当するロボットクラス(ここではLoginScreenRobotクラスのclickLogin()メソッド)を修正するだけで対応が可能です。

 fun clickLogin() {
-    composeRule.onNode(hasText("ログイン").and(hasClickAction()))
+    composeRule.onNode(hasText("サインイン").and(hasClickAction()))
         .performClick()
 }

おわりに

STORES ブランドアプリ のAndroidアプリでは、Robotパターンの導入により、効率的で保守性の高いE2Eテスト環境を構築しています。Robotパターンは、テストケースを拡充すればするほど、再利用性の面でも効果を発揮します。ぜひ、みなさんのプロジェクトにも取り入れてみてください。