UI Testing in Android — The Robot Pattern
一言で表すと
RobotパターンでわかりやすいUIテストを書こう
概要
Espresso で UI テストを書くことは、やったことがない人にとっては困難で直感的でないことがあります。
ロボットパターンなし
古いMVCスタイルでEspressoテストを書くことができるが、Android Studio のRun -> Record Espresso Testをクリックしてテストをレコーディングすると、以下のようなコードを生成してくれる。
なんだかごちゃごちゃしてわかりにくい。
参考
ロボットパターンあり
開発者としては技術的なことだけ考えてしまうが、QAやユーザの視点だとビジネスクリティカルなフローをテストができることが大事。
例えば、購入ボタンを押すと購入できることが大事で、ボタンをクリックした時にクリックイベントが走ることをテストしたい訳ではない。
Kotlin DSLとRobotパターンの助けを借りてUIテストを書きやすく、理解しやすく、誰でも簡単にメンテできるようにしよう。
Kotlin DSL
DSL(Domain-specific Language)とは、特定の応用領域に特化したコンピュータ言語である。
基本的には、特定のアプリケーションのユースケースに合わせて言語を最適化したり制約したりできることを意味し、DSLをゼロから書くのではなく、Kotlinを使って独自のDSLを書くだけなので、内部DSLを書くことになります。
より詳しい説明はこちら
Robot pattern
目標は、ビューで行う高レベルのアクションを実際のコード実装から抽象化することです。
投稿作成画面の例
入力欄
投稿ボタン
投稿結果の表示欄
https://miro.medium.com/max/720/1*o85aG9snWpwhJJ2_QCSWSw.png
Espressoで普通にテストを書くと以下のような感じ。
「Post Submitted.」が表示されることを期待している。
code:kotlin
class CreatePostActivityTest {
@Test
fun void createPost_WithText_Success() {
// Find the edit text and perform typing text action
// close the keyboard after text has been entered
onView(withId(R.id.post_text))
.perform(ViewActions.typeText("This is a post"),ViewActions.closeSoftKeyboard))
// Find the submit post button and perform a click
onView(withId(R.id.post_button))
.perform(ViewActions.click())
// Find the verification text and verify the result text
onView(withId(R.id.post_submit_result_text))
.check(ViewAssertions.matches(ViewMatchers.withText(R.string.post_submitted)))
}
}
これをRobotパターンでテストできるようにする。
便利な関数を書く
code:kotlin
fun ViewInteraction.click(): ViewInteraction =
perform(ViewActions.click())
fun ViewInteraction.typeText(text: String): ViewInteraction =
perform(ViewActions.typeText(text), ViewActions.closeSoftKeyboard())
fun ViewInteraction.isTextDisplayed(text: String): ViewInteraction =
check(ViewAssertions.matches(ViewMatchers.withText(text)))
BaseTestを書く
Kotlin DSLを活用する
code:kotlin
abstract class BaseTest {
val createPost = robotRunner(CreatePostRobot::class)
private fun <T : BaseRobot> robotRunner(cls: KClass<T>) = { func: T.() -> Unit ->
cls.createInstance().apply {
func()
}
}
}
Robotを書く
mkeeda.icon PageObjectパターンと似た雰囲気を感じる
code:kotlin
class CreatePostRobot : BaseRobot() {
fun enterPostText(text: String) =
postText().typeText(text)
fun submitPost() =
postButton().click()
fun matchResultText(expectedText: String) =
postSubmitResultText().isTextDisplayed(expectedText)
companion object {
fun postText(): ViewInteraction =
onView(withId(R.id.post_text))
fun postButton(): ViewInteraction =
onView(withId(R.id.post_button))
fun postSubmitResultText(): ViewInteraction =
onView(withId(R.id.post_submit_result_text))
}
}
code:kotlin
@RunWith(AndroidJUnit4::class)
class CreatePostActivityTest : BaseTest() {
@Rule
@JvmField
var activityTestRule = ActivityTestRule(CreatePostActivity::class.java)
@Test
fun createPost_WithText_Success() {
createPost {
enterPostText("Hello World!")
submitPost()
matchResultText(activityTestRule.getString(R.string.post_submitted))
}
}
}
実装からアクションを抽象化したことで、最初のコードと比較して、より読みやすくなり、誰でも何が起こっているのか理解できるようになりました。
何をテストしているのか、どのようにテストしているのかを分離しました。
このテストでは、ユーザーがEditTexにテキストを入力し、送信ボタンをクリックしてテキストを送信し、最後にTextViewのテキストをチェックしてテキストが正常に送信されたことを確認することをテストしています。
<ImageViewのケースについて>
私たちは、テストコードが、誰も触りたくない、作り方を知らないスパゲッティの山である必要はないことを知りました。コードをちらっと見るだけで、何をテストしているかがわかり、根本的な実装はもう重要ではありません。最終的には、読みやすく、メンテナンスしやすく、適切に設計されたテストスイートができあがり、どんなエンジニアも喜んでテストを追加してくれることでしょう。
気になるポイント
メモ
コメント
Go.icon最終系がDSLで書けるなら、UIテストも怖くない気がしてきた
chigichan24.icon これで書いていくとBaseTestにcreatePostみたいなのがすべてが集約して巨大化しそうな予感がある
rule化して外からクラスを注入できるようにすれば良い説
mkeeda.icon QAさんとか専門スキルが高くない人にも読めるのが大切なのかな?
RyuNen344.iconよく言われるテストはどういうテストなのか説明するだけにしようという意思を感じる(Kotest的な)