こんにちは、naberyo (@error96num) です。今年の4月に STORES へ入社し、 STORES ブランドアプリ のAndroidエンジニアをしています。
前回の記事では、 STORES ブランドアプリ におけるE2Eテスト(End-to-Endテスト)導入の背景とプロセスについて紹介しました。
本記事では、Android アプリのE2Eテスト実装で、可読性・再利用性・保守性を向上するために採用した Robotパターンの考え方と、Compose UI Testを使った具体的な実装例を紹介します。
Robotパターンとは?
Robotパターンは、テスト対象の画面やフローごとに「ロボット」クラスを定義し、ユーザー操作を模倣するメソッドを集約する設計パターンです。このパターンを採用することで、以下のようなメリットがあります。
- 可読性: テストケースが「何をテストしているのか」を一目で理解しやすくなる。
- 再利用性: 各画面やフローのロジックをカプセル化することで、他のテストケースでも再利用できる。
- 保守性: 特定の画面やフローに変更があった場合でも、基本的には該当するロボットクラスの修正だけで対応できる。
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パターンは、テストケースを拡充すればするほど、再利用性の面でも効果を発揮します。ぜひ、みなさんのプロジェクトにも取り入れてみてください。