MyFamilyTracker
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
How I test Compose UIs without the suite turning brittle — what to assert, how the testing APIs work, the synchronization model that makes flakiness rare, and where UI tests stop earning their keep.
On this page
UI tests have a bad reputation: slow, flaky, and forever breaking when you move a button. Compose's testing tools earn back a lot of that trust, but only if you test the right things. The trick is to assert behavior a user cares about, not pixel positions or internal structure. Done that way, Compose tests catch real regressions and survive refactors.
Almost every Compose test is a variation on the same shape: find a node, do something to it, assert a result. You find nodes by what the user perceives — text, content description, a test tag — not by type or position.
@get:Rule val rule = createComposeRule()
@Test
fun submitButton_disabledUntilFormValid() {
rule.setContent { SignUpScreen() }
rule.onNodeWithText("Sign up").assertIsNotEnabled()
rule.onNodeWithContentDescription("Email").performTextInput("a@b.com")
rule.onNodeWithContentDescription("Password").performTextInput("secret12")
rule.onNodeWithText("Sign up").assertIsEnabled()
}That test reads like a user story, and it will keep passing even if you completely rewrite the layout, because it depends on behavior, not structure.
The usual source of UI-test flakiness is timing — asserting before the screen has settled. Compose's test rule automatically waits for recomposition and idle state before each assertion, so you rarely write manual sleeps or waits. When you collect a flow, animate, or load async data, the rule synchronizes with Compose's own work clock. This is the single biggest reason Compose tests are less flaky than the old Espresso-on-Views world: the framework knows when it's busy and the test waits for it.
When you do drive async state, advance it deterministically rather than sleeping. Pair the test with a test dispatcher so coroutines run on a clock you control, and the result is a test that's fast and never racy.
It's tempting to slap a
testTagtestTagThe most valuable tests in a Compose app usually aren't UI tests at all. Logic — validation, state transitions, error handling — belongs in the ViewModel, and the ViewModel is a plain class you can test with no UI, no device, and no synchronization concerns. Those tests run in milliseconds. I push as much behavior as possible into the ViewModel precisely so I can test it cheaply, and then I write a thin layer of Compose tests to confirm the screen renders each state and forwards each interaction. The UI test answers "does the screen show the error state," and the unit test answers "does an invalid input produce the error state." Splitting those questions keeps each test simple.
UI tests are slower than unit tests by orders of magnitude, so I don't try to cover every combination through the UI. I test the critical user flows — sign-up, the core action, a couple of error paths — and rely on ViewModel unit tests for the combinatorial detail. Trying to test every edge case through the UI is how a suite becomes a twenty-minute drag that people start skipping. The goal isn't maximum UI coverage; it's confidence that the screens wire up correctly to logic you've already tested thoroughly elsewhere. A small, fast, behavior-focused UI suite that people actually run beats an exhaustive one that rots in a disabled CI job.
The mindset that makes all of this sustainable is treating tests as documentation of behavior rather than a coverage metric to satisfy. A good Compose test reads like a description of what the screen promises a user, and when it fails it tells you which promise broke. That framing keeps you from writing brittle tests that assert implementation details, because implementation details aren't promises — they're how you happen to keep them today. Aim for a suite where every test would still make sense if you handed it to someone who'd never seen the code, and you'll naturally end up with the kind of behavior-focused tests that survive refactors and actually get maintained.
testTagSudarshan Chaudhari
AI Systems Builder / Product Engineer
Bangkok, Thailand
Solo Android developer with 13+ years in QA, building Android apps, AI automation systems, and developer tools at SudarshanTechLabs.
Related Posts
Related Apps
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
Building something? Available for Android dev and QA consulting.
Work with meComments — powered by Giscus
Real-time family location sharing — Firebase Realtime DB for sub-second propagation, WorkManager + ForegroundService for OS-compliant background collection, geofencing via Google Maps API.
ReadPrivate dream journal — structured entry capture, pattern tagging, and optional Claude-powered insight generation. All data stays on-device by default.
ReadWorkout tracker — exercise logging with set/rep/weight history, goal progression, and local Room DB persistence. No account, no cloud sync required.
Read