Ui testing patterns
Skill almasumdev/awesome-mobile-testing-agent-skills/.github/skills/ui/ui-testing-patterns
Compare and apply UI test tooling across mobile — Espresso, XCUITest, flutter_test / integration_test, Detox, and Maestro. Use when deciding which UI test layer fits a feature and how to structure selectors, waits, and page objects.From its SKILL.md
npx -y skills add almasumdev/awesome-mobile-testing-agent-skills --skill ui-testing-patternsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
2 things to look at
- no licenseNo license file was found in the repository. Code published without one is not open source by default, so using it at work is a question for whoever answers licensing questions where you are.
- 1 stars1 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
SKILL.md
5.4 KB, ~1.3k tokens by cl100k_base, as published. Nobody here has run it
UI Testing Patterns Across Mobile
Instructions
UI tests are the most expensive layer to run and to maintain. Pick the lightest tool that still covers the risk, write them in a page-object / screen-robot style, and keep selectors stable.
1. Pick the Right Tool
| Tool | Scope | Runs On | Best For |
|---|---|---|---|
| Espresso / Compose UI test | Android, in-process | Emulator / device | Single-screen behavior, fast iteration |
| Robolectric + Compose | Android, JVM | Host JVM | Compose component tests without an emulator |
| XCUITest | iOS, out-of-process | Simulator / device | End-to-end iOS flows |
| ViewInspector / SwiftUI Previews + snapshot | iOS, in-process | Simulator | SwiftUI component tests |
flutter_test (widget tests) | Flutter | Host Dart VM | Widget-level, fast |
integration_test | Flutter | Emulator / device | Real-device smoke |
| Detox | React Native | Emulator / device | Full RN E2E in JS |
| Maestro | Any mobile app | Emulator / device / real | Black-box YAML smoke flows, cross-platform |
Rule of thumb: prefer in-process component tests (Compose, SwiftUI views, flutter_test, RN Testing Library) for 80 % of UI coverage; reserve out-of-process (XCUITest, Detox, Maestro) for smoke and critical paths.
2. Selector Strategy
Tests break most often because someone changed a string or a layout. Use stable, semantic selectors:
- Android Views:
R.id.*resource IDs. - Android Compose:
Modifier.testTag("checkoutPayButton")+onNodeWithTag. - iOS UIKit:
accessibilityIdentifier. - iOS SwiftUI:
.accessibilityIdentifier("checkoutPayButton"). - Flutter:
Key('checkoutPayButton')/ValueKey. - React Native:
testID="checkoutPayButton"+getByTestId.
Never select by visible text unless the text is the thing under test (e.g., a localization check).
3. Page-Object / Screen-Robot Pattern
Wrap each screen in a test-only API so tests read like business steps, not framework calls.
Kotlin + Compose:
class CheckoutRobot(private val rule: ComposeTestRule) {
fun enterCard(number: String) = apply {
rule.onNodeWithTag("cardNumber").performTextInput(number)
}
fun tapPay() = apply { rule.onNodeWithTag("pay").performClick() }
fun assertSuccess() = apply { rule.onNodeWithTag("success").assertIsDisplayed() }
}
@Test fun happyPath() = composeRule.run {
CheckoutRobot(this).enterCard("4242...").tapPay().assertSuccess()
}
Swift + XCUITest:
struct CheckoutScreen {
let app: XCUIApplication
@discardableResult func enterCard(_ s: String) -> Self {
app.textFields["cardNumber"].tap()
app.textFields["cardNumber"].typeText(s); return self
}
@discardableResult func tapPay() -> Self { app.buttons["pay"].tap(); return self }
func assertSuccess() { XCTAssertTrue(app.staticTexts["success"].waitForExistence(timeout: 5)) }
}
Dart + flutter_test:
class CheckoutRobot {
CheckoutRobot(this.t);
final WidgetTester t;
Future<void> enterCard(String s) async {
await t.enterText(find.byKey(const Key('cardNumber')), s);
}
Future<void> tapPay() async => t.tap(find.byKey(const Key('pay')));
void assertSuccess() => expect(find.byKey(const Key('success')), findsOneWidget);
}
4. Waits — No sleep
Use the framework's own "wait until condition" primitive:
- Espresso —
IdlingResourceplus sensibleonView(...).check(matches(...))which blocks on idling. - Compose —
waitForIdle,waitUntil { composeRule.onAllNodesWithTag(...).fetchSemanticsNodes().isNotEmpty() }. - XCUITest —
waitForExistence(timeout:),XCTNSPredicateExpectation. - Flutter —
pumpAndSettle()(scoped timeout) or an explicitpumploop with a deadline. - Detox —
waitFor(element(...)).toBeVisible().withTimeout(5000).
Never Thread.sleep / Task.sleep / await Future.delayed in UI tests. Waits must be condition-based.
5. Isolate from the World
Swap the network layer with a recorded or mocked server for the whole suite; swap the clock; seed a known user in local storage during @Before. UI tests must not depend on staging.
6. Cross-Platform Comparison
- Detox vs Maestro: Detox is JS-first and lives with the RN codebase; it covers complex flows with code. Maestro is YAML-first and works on any stack — prefer Maestro for smoke, Detox for deep RN-specific flows.
- Flutter
integration_testvs Maestro:integration_testgives Dart-level access and test semantics; Maestro is more robust on CI device farms and produces cleaner videos.
7. Failure Artifacts
Every UI test failure must produce: screenshot, view hierarchy / semantics dump, logs (logcat / os_log / Flutter debugDumpApp). Make that automatic in a @AfterEach / tearDown hook and upload from CI.
8. Checklist
- Selectors are test IDs / keys / accessibility identifiers — never raw visible text.
- Screens are wrapped in page objects / robots.
- No
sleep-based waits anywhere. - Network, time, and storage are isolated from real services.
- Failure produces screenshot + hierarchy + logs as CI artifacts.
- Each flow runs in under ~60 s; if longer, split it.
- Smoke vs regression suites are tagged and run on different cadences.
What ships with it
Read from the repository
Just SKILL.md. No reference files, no scripts.