184 lines
4.8 KiB
Markdown
184 lines
4.8 KiB
Markdown
---
|
|
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]]
|