はじめに
この記事はand factory.inc Advent Calendar 2025 11日目の記事です。
昨日は@nagata1121さんの[Android] CoilでIntrinsicSizeによる画像サイズ指定が効かない時の対処法 でした。
概要
Jetpack Composeのテストで「なぜかノードが見つからない!」という経験はありませんか?それはセマンティクスツリーのマージが原因かもしれません。
🔍 セマンティクスツリーのマージとは?
Composeは、パフォーマンスとアクセシビリティの最適化のために、複数のUIノードを1つのセマンティクスノードにマージすることがあります。
マージが発生する条件
Clickable要素(Button、Surface with onClick など)の子要素はマージされます。
// このような構造の場合
Surface(onClick = { /* クリック処理 */ }) {
Row {
Icon(...) // これらの要素は
Text(...) // 1つのノードに
Icon(...) // マージされる
}
}
❌ マージによって起こる問題
- testTagが見つからない
// ❌ このテストは失敗する
@Composable
fun CardWithTag() {
Surface(onClick = { /* ... */ }) {
Text(
text = "タイトル",
modifier = Modifier.testTag("title_text") // このタグは見つからない
)
}
}
@Test
fun testTagでノードを見つける() {
composeTestRule.setContent { CardWithTag() }
// ❌ 失敗:マージされてtestTagが消える
composeTestRule.onNodeWithTag("title_text").assertIsDisplayed()
// ✅ 成功:useUnmergedTreeを使う
composeTestRule.onNodeWithTag("title_text", useUnmergedTree = true).assertIsDisplayed()
// ✅ 成功:nodeWithTextなら見るかる
composeTestRule.onNodeWithText("タイトル").assertIsDisplayed()
}
- contentDescriptionが結合される
// ❌ 複数のIconのcontentDescriptionがマージされる
@Composable
fun IconRow() {
Surface(onClick = { /* ... */ }) {
Row {
Icon(
imageVector = Icons.Default.Star,
contentDescription = "お気に入り"
)
Icon(
imageVector = Icons.Default.Share,
contentDescription = "シェア"
)
}
}
}
@Test
fun 個別のIconを見つける() {
composeTestRule.setContent { IconRow() }
// ❌ 失敗:contentDescriptionが「お気に入り シェア」のように結合される
composeTestRule.onNodeWithContentDescription("シェア").performClick()
// ✅ 成功:useUnmergedTreeを使う
composeTestRule.onNode(
hasContentDescription("シェア"),
useUnmergedTree = true
).performClick()
}
✅ マージされても大丈夫なケース
Text要素の内容は検索可能
@Composable
fun CardWithText() {
Surface(onClick = { /* ... */ }) {
Column {
Text("タイトル") // このテキストは
Text("説明文") // マージ後も
Text("2024年12月") // 検索可能!
}
}
}
@Test
fun テキスト内容で検索() {
composeTestRule.setContent { CardWithText() }
// ✅ 成功:Textの内容はマージ後も検索できる
composeTestRule.onNodeWithText("タイトル").assertIsDisplayed()
composeTestRule.onNodeWithText("説明文").performClick()
}
📊 まとめ表
| 検索方法 | マージ後の動作 | useUnmergedTreeが必要? |
|---|---|---|
| onNodeWithText() | ✅ 検索可能 | 不要 |
| onNodeWithTag() | ❌ 見つからない | 必要 |
| onNodeWithContentDescription() | ⚠️ 結合される | 必要(個別に探す場合) |
🎯 使い分けのベストプラクティス
- 基本はマージされたツリーを使う
- パフォーマンスが良い
- 実際のユーザー操作に近い
- useUnmergedTree = trueを使うケース
- 特定の子要素だけをテストしたい
- testTagで個別要素を識別したい
- 複数のIconから特定のものを選びたい
- ノードの階層構造を厳密にテストしたい
- Textの検索は通常のツリーでOK
- onNodeWithText()は多くの場合そのまま使える
おわりに
いかがでしたでしょうか?
useUnmergedTreeを使うべきところ、そうでないところを理解してComposeのテスト書いていきたいですね。
明日の
Andfactory Advent Calendarもよろしくお願いします。 ![]()
🔗 Links