--- id: mobile-e2e-testing title: Mobile E2E Testing — Detox / Maestro / Appium category: Coding status: draft source_trust_level: B verification_status: conceptual created_at: 2026-05-09 updated_at: 2026-05-09 tags: [mobile, testing, detox, maestro, vibe-coding] tech_stack: { language: "TS / YAML", applicable_to: ["iOS", "Android", "React Native"] } applied_in: [] aliases: [Detox, Maestro, Appium, EarlGrey, mobile UI test, instrumented test] --- # Mobile E2E Testing > 단위 테스트 충분 X — 실기 / 시뮬레이터 UI 흐름. **Maestro = 가장 단순 (YAML)**, **Detox = RN 친화 (gray-box)**, **Appium = native 강력하지만 무거움**. ## 📖 핵심 개념 - E2E: 실제 앱 빌드 + 시뮬레이터 / 디바이스 + UI 조작. - Gray-box (Detox): JS 와 native 사이 연결 — RN 앱 idle 대기 자동. - Black-box (Appium / Maestro): UI 만 보고 조작. - Flake: UI 비동기 — wait / retry 가 핵심. ## 💻 코드 패턴 ### Maestro (YAML, 가장 단순) ```yaml # .maestro/login.yaml appId: com.acme.app --- - launchApp - tapOn: "Email" - inputText: "test@acme.com" - tapOn: "Password" - inputText: "password" - tapOn: "Sign in" - assertVisible: "Welcome" - takeScreenshot: home ``` ```bash maestro test .maestro/ maestro studio # 인터랙티브 record ``` ### Detox (RN 친화) ```js // e2e/login.test.js describe('Login', () => { beforeAll(async () => { await device.launchApp(); }); beforeEach(async () => { await device.reloadReactNative(); }); it('should login', async () => { await element(by.id('email')).typeText('test@acme.com'); await element(by.id('password')).typeText('password'); await element(by.id('login')).tap(); await waitFor(element(by.id('home'))) .toBeVisible() .withTimeout(5000); }); }); ``` ```bash detox build -c ios.sim.debug detox test -c ios.sim.debug ``` ### Appium (native + WebDriver) ```ts import { remote } from 'webdriverio'; const driver = await remote({ port: 4723, capabilities: { platformName: 'iOS', 'appium:platformVersion': '17.0', 'appium:deviceName': 'iPhone 15', 'appium:app': '/path/to/App.app', 'appium:automationName': 'XCUITest', }, }); await driver.$('~email').setValue('test@acme.com'); await driver.$('~login').click(); const welcome = await driver.$('~Welcome'); await welcome.waitForDisplayed({ timeout: 5000 }); ``` ### iOS native (XCUITest) ```swift class LoginUITests: XCTestCase { func testLogin() { let app = XCUIApplication() app.launch() app.textFields["email"].tap() app.textFields["email"].typeText("a@b.com") app.secureTextFields["password"].typeText("pw") app.buttons["Sign in"].tap() XCTAssertTrue(app.staticTexts["Welcome"].waitForExistence(timeout: 5)) } } ``` ### Android native (Espresso) ```kotlin @Test fun testLogin() { onView(withId(R.id.email)).perform(typeText("a@b.com")) onView(withId(R.id.password)).perform(typeText("pw")) onView(withId(R.id.login)).perform(click()) onView(withId(R.id.welcome)).check(matches(isDisplayed())) } ``` ### Flake 줄이기 ```ts // 명시적 wait — 임의 sleep 금지 await waitFor(element(by.id('home'))).toBeVisible().withTimeout(10000); // 네트워크 mock (Detox) await device.setURLBlacklist(['.*analytics.*']); // 시간 / animation off await device.setStatusBar({ time: '12:00' }); ``` ### Visual regression ```bash # Maestro - assertVisible: text: "Welcome" - takeScreenshot: home # 비교 = 자체 도구 (Percy, Applitools, BackstopJS) ``` ### CI (GitHub Actions) ```yaml - name: Build iOS run: detox build -c ios.sim.release - name: Test iOS run: detox test -c ios.sim.release --record-videos failing - uses: actions/upload-artifact@v4 if: failure() with: { path: artifacts/ } ``` ### Cloud test labs - BrowserStack / Sauce Labs / Firebase Test Lab — 다양 디바이스. - Maestro Cloud: Maestro 전용. ## 🤔 의사결정 기준 | 상황 | 추천 | |---|---| | RN 앱 | Detox 또는 Maestro | | 빠른 시작 / QA 친화 | Maestro | | 강력 / WebDriver 친숙 | Appium | | iOS native only | XCUITest | | Android native only | Espresso | | 다양 디바이스 | BrowserStack / Sauce / Firebase | | Visual regression | Maestro + Percy / Applitools | ## ❌ 안티패턴 - **Sleep 으로 wait**: flake 폭발. waitFor. - **Real network call**: 외부 의존 + flake. mock / stub. - **모든 path E2E**: 느림. critical path 만 (5-10). - **Test ID 없음 (텍스트로만 식별)**: i18n 시 깨짐. - **CI 만 — 로컬 안 됨**: 디버그 어려움. - **Snapshot 없는 실패**: 원인 추측. video / screenshot. - **Flaky 무시**: 1% flake 가 100 test = 매번 깨짐. ## 🤖 LLM 활용 힌트 - 새 RN 앱 = Maestro 시작. - testID 항상 부여 + waitFor. - CI artifact (video, screenshot) 필수. ## 🔗 관련 문서 - [[Mobile_CI_CD_Fastlane]] - [[Testing_Test_Pyramid]] - [[Frontend_A11y_Testing]]