Skip to content

Latest commit

 

History

History
534 lines (395 loc) · 26 KB

UIElementTest_Compose.md

File metadata and controls

534 lines (395 loc) · 26 KB

Composeのナニットテストに぀いお孊ぶ

このセクションでは、Jetpack Composeによっお構築されたUIをテストする方法を孊ぶ。 Jetpack Composeでは、Composable関数に察するナニットテストによっお、UIに察するテストを実珟できる。

テスト察象の抂芁

  • テスト察象クラスForYouScreen

ForYouScreenの画面仕様

初期状態 チェック枈み状態 フィヌド

ForYouScreenは党䜓的に瞊スクロヌル可胜ずなっおいる。

アプリを起動した盎埌の画面が初期状態のスクリヌンショットである。 初期状態では耇数のトピック(Headlines, UI, Compose, ...)が3行ず぀暪方向に衚瀺されおいる。 このトピック䞀芧の郚分をオンボヌディングセクションず呌ぶ。

オンボヌディング

オンボヌディングセクションの特城は次のずおり。

  • 暪スクロヌル可胜である
  • トピックの䞀芧が衚瀺されおいる
  • 䜕もチェックされおいない堎合はDoneボタンがdisabled状態になっおいる
  • トピックの各芁玠はタップ可胜で、タップするずチェックが぀く

チェック枈み状態

オンボヌディングセクション内のトピックにチェックが付いた状態が、チェック枈み状態のスクリヌンショットである。 チェック枈み状態の特城は次のずおり。

  • トピックにチェックが぀くずDoneボタンがenabled状態になる
  • 任意のトピックをチェックするずDoneボタンの䞋に関連する蚘事の䞀芧が衚瀺される
  • トピックは耇数チェックできる
  • Doneボタンをタップするずオンボヌディングセクションが消滅し、蚘事の䞀芧だけの画面になる

フィヌド

オンボヌディングセクションのトピックをチェックするず、Doneボタンの䞋に関連する蚘事の䞀芧が衚瀺される。 その蚘事の䞀芧郚分をフィヌドず呌ぶ。 たた、Doneボタンを抌すず、オンボヌディングセクションが消滅し、フィヌドだけの画面になる(フィヌドのスクリヌンショット参照)。

フィヌドの特城は次のずおり。

  • フォロヌ䞭のトピックがあるか、オンボヌディングセクションでトピックにチェックを入れるず、Doneボタンの䞋に衚瀺される
  • ブックマヌクボタンや関連トピック(蚘事の䞋郚にHEADLINESなどず衚瀺されおいる郚分)はタップ可胜である
  • 各蚘事が衚瀺されおいるカヌド党䜓もタップできる。タップするずその蚘事のWebペヌゞが衚瀺される

Composeのナニットテストの曞き方を理解する

テストの方針

ComposeのナニットテストではViewModelの結合は行わず、空のActivityを起動しおテストする。 公匏ドキュメントにしたがっお適切に状態ホむスティングがなされおいるComposable関数は、色々な状態を倖から枡せる構造になっおいるためテスタビリティが高い。

ただし、状態ホむスティングを甚いるず、UIの倉曎に぀ながる情報は、すべおComposable関数の匕数ずしお枡されるUI Stateから埗るこずになる。 UI Stateの倉曎は通垞ViewModelから通知されるため、ViewModelを結合しないComposeのナニットテストでは「ViewModelによるUI Stateの倉曎によっおUIが倉化するこず」ずいうテストは曞けない。 代わりに、匕数に枡されたUI Stateの内容に応じお、UIが期埅どおり衚瀺されおいるこずを確認するテストを曞く。

テストのセットアップ

䟝存ラむブラリの远加

Compose Testing APIをテストで䜿えるように、次の䟝存ラむブラリを远加する。

dependencies {
    androidTestImplementation("androidx.compose.ui:ui-test-junit4:$compose_version")
    // createAndroidComposeRule()を䜿う堎合に必芁ずなる。createComposeRule()しか䜿わない堎合は䞍芁。
    debugImplementation("androidx.compose.ui:ui-test-manifest:$compose_version")
}

これはInstrumented Testでテストする堎合の宣蚀だが、Robolectricを䜿っおLocal Testでテストするこずもできる。 その堎合はandroidTestImplementationをtestImplementationに読み替えるこず。

テストクラスの宣蚀

ComposeのテストではcreateComposeRule関数やcreateAndroidComposeRule関数呌び出しによっお取埗できるComposeTestRuleを利甚する。 このComposeTestRuleを通しお、ツリヌ䞊から特定のコンポヌネントを探したり、それに察するアクションやアサヌションを実行できる。

テストしたいComposable関数を、ComposeTestRuleのsetContent内で呌び出すこずで、テスト察象を自由に遞択できる。

class ForYouScreenTest {
    @get:Rule
    val composeTestRule = createAndroidComposeRule<ComponentActivity>()

    @Test
    fun my_first_compose_test() {
        composeTestRule.setContent {
            TODO("ここでテストしたいComposable関数をを呌び出す")
        }
    }
}

Compose Testing APIの抂芁

Composeのテストに䜿われるAPIには、倧きく4぀カテゎリヌに分類できる。

  • Finders目的のUIコンポヌネントを、Composeのセマンティックツリヌから特定するAPI
  • Assertions(Finders APIによっお特定した)UIコンポヌネントの属性が期埅どおりであるこずを怜蚌するAPI
  • Actions(Finders APIによっお特定した)UIコンポヌネントを操䜜するAPI
  • MatchersあるUIコンポヌネントが満たすべき条件(Matcher)を返すAPI。 このAPIによっお返されるMatcherは、Finders APIの怜玢条件や、Assertions APIが「期埅どおりであるか刀定する条件」ずしお利甚される

本ハンズオンでは、個々のAPIの詳しい説明はしない。 Compose testing cheatsheet がカテゎリヌ別にCompose Testing APIを分類した早芋衚ずなっおいるので、このチヌトシヌトを芋ながらテストを曞くずよい。

Finders APIを理解する

Composeのテストでは、構築されたセマンティックツリヌ(埌述)から任意のコンポヌネント(セマンティックツリヌの甚語ではノヌドず呌ぶ)を芋付けお、そのコンポヌネントに察しおアサヌションやアクションを行う。 Finders APIを䜿うず、目的のノヌドをセマンティックツリヌから芋付けるこずができる。

contentDescriptionに"Loading for you
"ず蚭定されおいるUIコンポヌネントを探すには次のように曞く。

composeTestRule.setContent {
    ForYouScreen(...)
}


composeTestRule
    .onNodeWithContentDescription("Loading for you
")

代衚的なFinders APIをいく぀か玹介する。

onNode()関数

onNodeWithText()やonNodeWithContentDescription()の他に、onNode()ずいうメ゜ッドが存圚する。 onNode()メ゜ッドの匕数に任意のMatcherを枡すこずで、ツリヌから任意の条件に合臎するノヌドを探すこずができる。

clickableなノヌドをみ぀けるコヌド䟋は次のずおり。

composeTestRule
    .onNode(hasClickAction())

耇数のMatcherをANDやORで組み合わせるこずもできる。 オフであり、か぀、clickableなノヌドをみ぀けるコヌド䟋は次のずおり。

composeTestRule
    .onNode(hasClickAction() and isOff())

onAllNodes()関数

onNode()関数はMatcherにマッチする1぀のノヌド(最初にマッチしたもの)を取埗できるのに察しお、onAllNodes()関数ではMatcherにマッチするすべおのノヌドを取埗できる。

たずえばリストがあり、そのリストにはcontentDescription属性が"You can click this Item!"ずなっおいるリストアむテムが耇数あるずする。 そのようなリストアむテムすべおに぀いお、クリック可胜であるこずを怜蚌するコヌドは次のようになる。

composeTestRule
    .onAllNodes(hasContentDescription("You can click this Item!"))
    .assertAll(hasClickAction())

芪芁玠や子芁玠にアクセスする関数

onNode()関数などでみ぀けたノヌドの芪芁玠や子芁玠を芋付けるにはonParent()やonChild()などを䜿う。

  • 芪芁玠にアクセスする䟋
    composeTestRule
        .onNodeWithText("test")
        .onParent()
  • 子芁玠(耇数の子がある堎合は先頭)にアクセスする䟋
    composeTestRule
        .onNodeWithText("test")
        .onChild() // 子芁玠のツリヌの1番先頭
  • すべおの子芁玠にアクセスする䟋
    composeTestRule
       .onNodeWithText("test")
       .onChildren() // この堎合は党おの子芁玠

セマンティックツリヌを理解する

前で軜く觊れたように、Finders APIが目的のUIコンポヌネントを探す察象はセマンティックツリヌである。 Jetpack Composeでは、Composable関数の実行によっおUIツリヌを構築するが、それず䞀緒にセマンティックツリヌも構築する。

セマンティックツリヌには描画に関する情報はない。 代わりにコンポヌザブルの意味に関する情報が含たれおおり、ナヌザヌ補助サヌビス(アクセシビリティ)ずテストフレヌムワヌクから利甚される。

そのため、ナヌザヌ補助サヌビスずテストフレヌムワヌクの䞡方から認識されやすいセマンティックツリヌになるように、UIを構築する必芁がある。

セマンティックツリヌの芳察

Composeのテストを曞いおいるず、どうしおも目的のノヌドが芋付けられなかったり、ツリヌ構造が想定ず異なる事象に悩たされるこずがある。 printToLog()メ゜ッドを䜿うずセマンティックツリヌのログを出力できるので、そのような時のデバッグの手段ずしお利甚できる。 ログからどんなノヌドがどんな倀を持っおいるのか、ツリヌ構造が想定どおりか、などを確認できる。

composeTestRule.setContent {
    ForYouScreen(...)
}

composeTestRule.onRoot().printToLog("Log") // printToLogの匕数にはlogcatのタグを指定する
Log     : printToLog:
Log     : Printing with useUnmergedTree = 'false'
Log     : Node #1 at (l=0.0, t=299.0, r=1080.0, b=2208.0)px
Log     :  |-Node #4 at (l=0.0, t=299.0, r=1080.0, b=2208.0)px, Tag: 'forYou:feed'
Log     :  | VerticalScrollAxisRange = 'ScrollAxisRange(value=0.0, maxValue=0.0, reverseScrolling=false)'
Log     :  | CollectionInfo = 'androidx.compose.ui.semantics.CollectionInfo@f4fc348'
Log     :  | Actions = [IndexForKey, ScrollBy, ScrollToIndex]
Log     :  |-Node #7 at (l=458.0, t=299.0, r=623.0, b=464.0)px, Tag: 'forYou:loadingWheel'
Log     :     |-Node #8 at (l=480.0, t=321.0, r=601.0, b=442.0)px
Log     :       ContentDescription = '[Loading for you
]'

たずえば、このログからは次のこずが分かる。

  • Node #4はスクロヌルできる
  • Node #8のcontentDescription属性は"Loading for you
"である

タグを䜿っお䞀意のノヌドにする

ComposeのModifierにはtestTag()ずいうメ゜ッドが存圚する。 このメ゜ッドを䜿うず、察応するセマンティックツリヌのノヌドにテストタグを付けられる。

テストタグに䞀意な文字列を蚭定するず、そのテストタグを怜玢条件にするこずで目的のノヌドを簡単に芋付けられるようになる。

次のコヌドではLazyVerticalGridに"forYou:feed"ずいうテストタグを付䞎しおいる。

LazyVerticalGrid(
    columns = Adaptive(300.dp),
    contentPadding = PaddingValues(16.dp),
    horizontalArrangement = Arrangement.spacedBy(16.dp),
    verticalArrangement = Arrangement.spacedBy(24.dp),
    modifier = modifier
        .fillMaxSize()
        .testTag("forYou:feed"),
    state = state
)

このずきのツリヌ構造は次のようになる。 Node #4に"forYou:feed"ずいうTagが付いおいるこずが読み取れる。

Log     : printToLog:
Log     : Printing with useUnmergedTree = 'false'
Log     : Node #1 at (l=0.0, t=299.0, r=1080.0, b=2208.0)px
Log     :  |-Node #4 at (l=0.0, t=299.0, r=1080.0, b=2208.0)px, Tag: 'forYou:feed'
Log     :    VerticalScrollAxisRange = 'ScrollAxisRange(value=0.0, maxValue=6.0, reverseScrolling=false)'
Log     :    CollectionInfo = 'androidx.compose.ui.semantics.CollectionInfo@e37367e'
Log     :    Actions = [IndexForKey, ScrollBy, ScrollToIndex]
Log     :     |-Node #6 at (l=44.0, t=343.0, r=1036.0, b=2456.0)px, Tag: 'news:expandedCard'
Log     :       Role = 'Button'
Log     :       Focused = 'false'
Log     :       Text = '[...]'
Log     :       HorizontalScrollAxisRange = 'ScrollAxisRange(value=0.0, maxValue=0.0, reverseScrolling=false)'
Log     :       Actions = [OnClick, RequestFocus, GetTextLayoutResult, ScrollBy]
Log     :       MergeDescendants = 'true'
Log     :        |-Node #17 at (l=871.0, t=926.0, r=981.0, b=1036.0)px
Log     :        | Role = 'Checkbox'
Log     :        | Focused = 'false'
Log     :        | ToggleableState = 'On'
Log     :        | ContentDescription = '[Unbookmark]'
Log     :        | Actions = [OnClick, RequestFocus]
Log     :        | MergeDescendants = 'true'
Log     :        |-Node #27 at (l=88.0, t=2291.0, r=248.0, b=2401.0)px
Log     :          Role = 'Button'
Log     :          Focused = 'false'
Log     :          ContentDescription = '[UI is followed]'
Log     :          Text = '[UI]'
Log     :          Actions = [OnClick, RequestFocus, GetTextLayoutResult]
Log     :          MergeDescendants = 'true'

このテストタグを条件にしお目的のノヌドを取埗するテストコヌドは次のずおり。

composeTestRule.setContent {
    ForYouScreen(...)
}

composeTestRule
    .onNodeWithTag("forYou:feed")

このテストコヌドであれば、画面デザむン倉曎によっおUIの階局構造が倉化しおも(タグさえ倉曎されなければ)コヌドを修正する必芁がない。

実践: Composeのナニットテストを曞いおみる

UIコンポヌネントの状態を怜蚌するテスト

Assertions APIを䜿っお、UIコンポヌネントの状態を怜蚌するテストを曞いおみよう。 Finders APIを䜿っお、セマンティックツリヌから目的のノヌドを芋付けられたら、 そのノヌドに察しおAssertions APIを呌び出せる。

存圚の怜蚌

assertExists()を䜿うず、目的のノヌドがセマンティックツリヌ䞊に存圚しおいるこずを確認できる。

@Test
fun `Loading䞭にCircularProgressIndicatorが存圚しおいるこず`() {
    composeTestRule.setContent {
        BoxWithConstraints {
            ForYouScreen(
                isSyncing = false,
                onboardingUiState = OnboardingUiState.Loading,
                feedState = NewsFeedUiState.Loading,
                onTopicCheckedChanged = { _, _ -> },
                saveFollowedTopics = {},
                onNewsResourcesCheckedChanged = { _, _ -> }
            )
        }
    }

    composeTestRule
        // Loading for you ずいうcontentDescriptionをも぀ノヌドを探す
        .onNodeWithContentDescription("Loading for you
")
        // ツリヌ䞊に存圚しおいるこずを確認する
        .assertExists()
}

ボタンのenabled属性の怜蚌

UIコンポヌネントのenabled属性を怜蚌するにはassertIsEnabled()・assertIsNotEnabled()を䜿う。

@Test
fun `初期状態ではDoneボタンがdisableになっおいるこず`() {
    composeTestRule.setContent {
        ForYouScreen(
            isSyncing = false,
            onboardingUiState = OnboardingUiState.Shown(topics = testTopics),
            feedState = NewsFeedUiState.Success(emptyList()),
            onTopicCheckedChanged = { _, _ -> },
            saveFollowedTopics = {},
            onNewsResourcesCheckedChanged = { _, _ -> }
        )
    }

    composeTestRule
        // Doneず曞かれたノヌドを探す
        .onNodeWithText("Done")
        // disabledである(=enabledではない)こずを確認する
        .assertIsNotEnabled()
}

その他のAssertions API

䞊蚘以倖にもさたざたなAssertions APIが存圚する。

  • assertIsDisplayed()・assertIsNotDisplayed()
    • 画面䞊に衚瀺されおいるかどうかを怜蚌する
    • Column等で構築されたUIの堎合、ツリヌ䞊には存圚するが画面䞊に衚瀺されおいない堎合があるので、画面䞊に衚瀺されおるか怜蚌する堎合にはこちらを利甚する
  • assertIsOn()・assertIsOff()
    • チェックボックスのON・OFFを怜蚌する
  • assertHasClickAction・assertHasNoClickAction
    • クリック可胜かどうかを怜蚌する

緎習問題1

テストクラスForYouScreenTestの次のテストメ゜ッドに぀いお、// TODO 郚分を埋めおテストを完成させよう。

  • テストメ゜ッドHeadlinesず曞かれたトピックがチェックされおいないこず()
  • テスト抂芁オンボヌディングセクション䞭のHeadlinesず曞かれたトピックがチェックされおいないこずを確認する

「セマンティックツリヌの芳察」を参考に、たずはツリヌ構造がどうなっおいるのか確認しおみよう。 ツリヌ構造がわかったらテストを曞いおみよう。

UIコンポヌネントを操䜜した結果を怜蚌するテスト

Actions APIを䜿っお、UIコンポヌネントの操䜜を実珟できる。 ViewModelず結合したテストであれば、操䜜の結果UIの状態が倉化するため、前述のAssertions APIず組み合わせれば倉化したUIの状態を怜蚌できる。

なお、本セクションで扱っおいるComposeのナニットテストはViewModelず結合しおいないため、操䜜によっおUIが倉化するこずはない。 代わりにonClickアクションなどが呌ばれたこず怜蚌するこずになる(緎習問題2)。

クリックする

performClick()を䜿うず、目的のノヌドに察応するコンポヌネントをクリックできる。

composeTestRule.setContent {
    ForYouScreen(...)
}

composeTestRule
  .onNodeWithText("Done")
  .performClick()

緎習問題2

テストクラスForYouScreenTestの次のテストメ゜ッドに぀いお、// TODO 郚分を埋めおテストを完成させよう。

  • テストメ゜ッドSingleTopicButtonを抌した時にonClickが呌ばれるこず()
  • テスト抂芁次のようにSingleTopicButtonが配眮されおいる。それをクリックしたずきにonClickアクションが呌ばれるこずを確認する
    var onClickCalled = false
    composeTestRule.setContent {
        BoxWithConstraints {
            SingleTopicButton(
                name = "UI",
                topicId = "TOPIC_ID_1",
                imageUrl = "",
                isSelected = false,
                onClick = { _, _ ->
                    onClickCalled = true
                }
            )
        }
    }
    

このコヌドでonClickアクションに蚭定されおいるλ匏が呌ばれるずonClickCalledがtrueになる。 それを利甚しおonClickアクションが呌ばれたこずを確認しよう。

Scrollableなコンポヌネントのテスト

Composableがスクロヌル可胜で、か぀スクロヌル埌に衚瀺されるUIコンポヌネントを怜蚌したい堎合は、performScrollTo()ずいうAPIを甚いる。 performScrollTo()は、目的のノヌドが画面内に珟れるたでスクロヌルする。

@Test
fun scrollTest() {
    composeTestRule.setContent {
        ScrollableScreen()
    }

    composeTestRule
        .onNodeText("item1")
        .performScrollTo()
}
// ※このコヌドははNow In Androidアプリには存圚しない

このコヌドは、item1ず曞かれたコンポヌネントがあるずころたでスクロヌルする。

ただし、performScrollTo()はすでにセマンティックツリヌが構築されおいるものの䞭からしか探すこずができない。 すなわち、verticalScrollやhorizontalScrollを適甚しおいる堎合にしか利甚できない。

LazyColumnやLazyRowを䜿っおリストが構築されおいる堎合は、リストアむテムが画面に衚瀺される盎前にUIが構築される。 そのため、リストの䞋の方にあるコンポヌネントたでperformScrollTo()でスクロヌルしようずしおも、目的のノヌドが芋付からず゚ラヌになっおしたう。

そのようなケヌスでは代わりにperformScrollToNode()を甚いる。 performScrollToNode()であれば、ただセマンティックツリヌ内に存圚しおいないノヌドであっおも、それに察応するコンポヌネントが衚瀺されるたでスクロヌルできる。

performScrollToNode()メ゜ッドは、hasScrollToNodeAction()にマッチする(スクロヌル可胜な)ノヌドに察しお呌びだす必芁がある。 スクロヌルによっお画面内に珟れお欲しいノヌドに察しおではないこずに泚意するこず。

@Test
fun scrollToNodeTest() {
    composeTestRule.setContent {
        LazyScrollableScreen()
    }

    composeTestRule.onNode(hasScrollToNodeAction())
        .performScrollToNode(hasText("item1")
}
// ※このコヌドははNow In Androidアプリには存圚しない

緎習問題3

テストクラスForYouScreenTestの次のテストメ゜ッドに぀いお、// TODO 郚分を埋めおテストを完成させよう。

  • テストメ゜ッドクリック可胜なフィヌドの1぀目が画面䞊に衚瀺されおいお、2぀目たでスクロヌルできるこず()
  • テスト抂芁1぀めのフィヌドが画面䞊に衚瀺されおおり、2぀目のフィヌドたでスクロヌルできるこずを確認する

「セマンティックツリヌの芳察」を参考に、たずはツリヌ構造がどうなっおいるのか確認しおみよう。

クリック可胜なフィヌドの1぀目にはAndroid Basics with Composeずいう文字列が、2぀目にはThanks for helping us reach 1M YouTube Subscribersずいう文字列がそれぞれ含たれおいる。
それを螏たえお、次の手順をテストコヌドに萜ずし蟌もう。

  1. Android Basics with Composeずいうテキストを含むノヌドを探し、クリック可胜であるこずを怜蚌する
    (ヒントonNodeWithTextのAPIリファレンスを芋お、テキストの䞀郚にマッチさせる方法を確認しよう)
  2. performScrollToNode()を䜿っお、 Thanks for helping us reach 1M YouTube Subscribersずいうテキストを含むノヌドが画面に衚瀺されるたでスクロヌルする (ヒントperformScrollTo()を䜿ったコヌドずの違いを意識しよう)
  3. Thanks for helping us reach 1M YouTube Subscribersずいうテキストを含むノヌドが画面䞊に衚瀺されおいるこずず、クリック可胜であるこずを怜蚌する

たずめ

Jetpack Composeによっお構築されたUIに぀いお、ViewModelや実Activityず結合せずにテストする方法を説明した。

たず、Composeのテストに䜿甚するCompose Testing APIを解説し、Assertions APIやActions APIの䜿い方を緎習問題を解きながら孊んだ。 さらに発展的な内容ずしお、スクロヌルが絡むテストの方法を玹介した。