Wenn du mit Jetpack Compose UI Tests arbeitest, bist du wahrscheinlich schon über ein seltsames Verhalten gestolpert:
Du setzt ein testTag auf ein Composable – aber plötzlich findet dein Test den Node nicht mehr.
Der Grund ist häufig dieser Modifier:
Modifier.semantics(mergeDescendants = true)
Viele Entwickler nutzen ihn, ohne wirklich zu verstehen, was er mit der Semantik-Hierarchie macht.
In diesem Artikel schauen wir uns an:
- was Semantics in Compose überhaupt ist
- wie der Semantik-Baum aufgebaut ist
- was
mergeDescendants = truewirklich verändert - warum dadurch Testtags verschwinden
- wie du trotzdem Kinder testen kannst
1. Was ist Semantics in Jetpack Compose?
Semantics beschreibt die Bedeutung eines UI-Elements.
Sie wird hauptsächlich verwendet für:
- Accessibility (TalkBack / Screenreader)
- Compose UI Tests
Semantics ist also eine Meta-Beschreibung deiner UI, nicht das UI selbst.
Typische Semantics-Properties sind:
contentDescriptiontestTagstateDescriptionclickableenabledselected
Beispiel:
Box(
modifier = Modifier
.semantics {
testTag = "MyBox"
}
) {
Text("Hallo Welt")
}
Jetzt kann dein UI Test dieses Element finden:
composeTestRule
.onNodeWithTag("MyBox")
.assertExists()
2. Der Semantik-Baum
Compose baut intern einen Semantik-Baum, der parallel zum UI aufgebaut wird.
Beispiel:
Column {
Text(
"Hallo",
modifier = Modifier.semantics {
testTag = "Child1"
}
) Text(
"Welt",
modifier = Modifier.semantics {
testTag = "Child2"
}
)
}
Der Semantik-Baum sieht vereinfacht so aus:

Wichtig:
👉 Jedes Element hat seinen eigenen Semantik-Node
Deshalb funktionieren diese Tests problemlos:
composeTestRule.onNodeWithTag("Child1")
composeTestRule.onNodeWithTag("Child2")
3. Was macht mergeDescendants = true?
Wenn du mergeDescendants = true setzt, passiert Folgendes:
👉 Die Semantics der Kinder werden in den Parent integriert
Die Kinder existieren dann nicht mehr als eigene Semantik-Nodes.
Beispiel:
Column(
modifier = Modifier.semantics(mergeDescendants = true) {
testTag = "Parent"
}
) { Text(
"Hallo",
modifier = Modifier.semantics {
testTag = "Child1"
}
) Text(
"Welt",
modifier = Modifier.semantics {
testTag = "Child2"
}
)
}
Der Semantik-Baum wird jetzt zu:

Die Kinder sind nicht mehr separat vorhanden.
Das bedeutet:
❌ Dieser Test schlägt fehl
composeTestRule.onNodeWithTag("Child1")
weil der Node nicht mehr existiert.
4. Warum gibt es diese Funktion überhaupt?
Der Hauptgrund ist Accessibility.
Ohne Merge kann ein Screenreader sehr viele kleine Elemente vorlesen:
Text: Hallo
Text: Welt
Button
Icon
Text
Mit mergeDescendants = true kann Compose daraus ein einziges logisches Element machen.
Beispiel: Ein zusammengesetzter Button
Row(
modifier = Modifier
.clickable { }
.semantics(mergeDescendants = true) {}
) {
Icon(Icons.Default.Favorite)
Text("Like")
}
Screenreader liest jetzt:
Button: Like
statt
Icon
Text Like
5. Warum verschwinden Testtags?
Der Grund ist simpel:
UI Tests arbeiten auf dem Semantik-Baum.
Wenn mergeDescendants = true gesetzt ist:
- die Kinder-Nodes verschwinden
- ihre Semantics werden integriert
testTagbleibt nur auf dem Parent sichtbar
Deshalb können Tests Kinder nicht mehr direkt finden.
6. Wie testet man trotzdem Inhalte?
Obwohl die Kinder verschwinden, bleiben ihre Inhalte Teil der Parent-Semantik.
Du kannst also z.B. Text prüfen:
composeTestRule
.onNodeWithTag("Parent")
.assertTextContains("Hallo")
oder
composeTestRule
.onNodeWithTag("Parent")
.assertTextContains("Welt")
7. Trick: Testtags von Kindern in die Parent-Semantics übernehmen
Wenn du mergeDescendants brauchst, aber trotzdem Kinder identifizieren möchtest, kannst du deren Informationen in die Parent-Semantics übernehmen.
Beispiel:
Column(
modifier = Modifier.semantics(mergeDescendants = true) { testTag = "Parent" contentDescription = "Child1 Hallo Child2 Welt"
}
) {
Text("Hallo")
Text("Welt")
}
Dann kannst du im Test prüfen:
composeTestRule
.onNodeWithTag("Parent")
.assertContentDescriptionContains("Child1")
Eine andere Möglichkeit ist eine Custom Semantics Property.
val ChildTagKey = SemanticsPropertyKey<String>("ChildTag")
var SemanticsPropertyReceiver.childTag by ChildTagKey
Dann:
Modifier.semantics {
childTag = "Child1"
}
Das kann im Test gezielt geprüft werden.
8. Wann solltest du mergeDescendants verwenden?
Empfohlen für:
✔ komplexe UI Komponenten
✔ zusammengesetzte Buttons
✔ Accessibility Optimierung
Nicht ideal für:
❌ fein granulare UI Tests
❌ wenn du Kinder direkt selektieren willst
Fazit
mergeDescendants = true ist ein kleines Feature mit großer Wirkung.
Es verändert nicht das UI, sondern den Semantik-Baum, auf dem:
- Accessibility
- Compose UI Tests
basieren.
Die wichtigste Konsequenz:
👉 Kinder verschwinden als eigenständige Nodes
Wenn deine Tests plötzlich keine Nodes mehr finden, lohnt sich also immer ein Blick auf diesen Modifier.
