---
title: Detox 完整入門 — React Native 自動化神器 從 0 到 CI 整合
description: Detox 完整指南。React Native E2E 框架、灰盒測試原理、Sync 機制、Page Object 適配、Mocking、CI 整合（GitHub Actions + Firebase Test Lab）、跟 Appium 對比。
category: automation
tags: [detox, react-native, e2e, mobile-automation, gray-box]
date: 2026-06-21
faq:
  - q: Detox 跟 Appium 該選哪個？
    a: React Native app → Detox（灰盒、快 5-10x、官方支援）。Native iOS / Android 或混搭 → Appium（通用但慢）。混合 hybrid app → Appium 含 webview 模式。
  - q: 為什麼 Detox 比 Appium 快這麼多？
    a: Appium 透過 WebDriver protocol、跨 process 通訊。Detox 直接注入 app process（灰盒）、能 sync app idle state、跳過 sleep。一個 test 平均省 50-70% 時間。
  - q: Detox 支援真機嗎？
    a: 過去只支援 Simulator / Emulator、2024 後 iOS 真機支援度提升、Android 真機需另設定。多數團隊跑 emulator + Firebase Test Lab 真機補強。
  - q: 不是 React Native 能用 Detox 嗎？
    a: 技術上可以（純 Native 也行）但意義不大。Detox 強項是 RN 同步、純 Native 用 XCUITest + Espresso 比較順。
---

# Detox 完整入門 — React Native 自動化神器

「我們 RN app 用 Appium 跑、每個 case 20 秒」 → 你浪費了 80% 時間。**Detox 為 RN 量身打造、灰盒測試、自動 sync app idle**，同 case 跑 4 秒。這篇給你 0 到 CI 完整 setup。

## 為什麼 Detox 比 Appium 快

```mermaid
flowchart LR
    A[Appium 黑盒] --> AP[透過 WebDriver]
    AP --> AS[跨 process 通訊]
    AS --> AT["需自己 wait<br>每動作 sleep / retry"]
    AT --> Slow["20s / case"]

    D[Detox 灰盒] --> DI[直接注入 RN process]
    DI --> DS[Auto-sync app idle state]
    DS --> DT["JS thread idle → 立刻動<br>不用 sleep"]
    DT --> Fast["4s / case"]

    style Slow fill:#ef4444,color:#fff
    style Fast fill:#10b981,color:#fff
```

**核心**：Detox 知道 RN「現在閒了」、立刻執行下一動作；Appium 只能 sleep + retry。

## 30 分鐘 0 到綠 CI

### Setup

```bash
# 1. Detox CLI
npm install -g detox-cli

# 2. 專案內裝
cd MyRNApp
npm install -D detox jest @types/jest

# 3. Init
detox init
```

會建：
- `.detoxrc.js` — 設定檔
- `e2e/jest.config.js` — Jest 配置
- `e2e/starter.test.js` — 範例 test

### .detoxrc.js 配置

```javascript
module.exports = {
  apps: {
    'ios.debug': {
      type: 'ios.app',
      binaryPath: 'ios/build/Build/Products/Debug-iphonesimulator/MyApp.app',
      build: 'xcodebuild -workspace ios/MyApp.xcworkspace -scheme MyApp -configuration Debug -sdk iphonesimulator -derivedDataPath ios/build',
    },
    'android.debug': {
      type: 'android.apk',
      binaryPath: 'android/app/build/outputs/apk/debug/app-debug.apk',
      build: 'cd android && ./gradlew assembleDebug assembleAndroidTest -DtestBuildType=debug',
      reversePorts: [8081],
    },
  },
  devices: {
    simulator: { type: 'ios.simulator', device: { type: 'iPhone 15' } },
    emulator: { type: 'android.emulator', device: { avdName: 'Pixel_7_API_34' } },
  },
  configurations: {
    'ios.sim.debug': { device: 'simulator', app: 'ios.debug' },
    'android.emu.debug': { device: 'emulator', app: 'android.debug' },
  },
};
```

### 第一個 test

```javascript
// e2e/login.test.js
describe('Login flow', () => {
  beforeAll(async () => {
    await device.launchApp();
  });

  beforeEach(async () => {
    await device.reloadReactNative();
  });

  it('登入成功跳轉 Home', async () => {
    await element(by.id('email-input')).typeText('qa@test.com');
    await element(by.id('password-input')).typeText('Test@123');
    await element(by.id('login-button')).tap();
    await expect(element(by.text('Welcome'))).toBeVisible();
  });

  it('錯密碼顯示錯誤', async () => {
    await element(by.id('email-input')).typeText('qa@test.com');
    await element(by.id('password-input')).typeText('wrong');
    await element(by.id('login-button')).tap();
    await expect(element(by.text('帳號或密碼錯誤'))).toBeVisible();
  });
});
```

### 跑

```bash
# Build
detox build -c ios.sim.debug

# Test
detox test -c ios.sim.debug

# Android
detox build -c android.emu.debug
detox test -c android.emu.debug
```

## Selector 哲學

```mermaid
flowchart LR
    Sel[Detox Selector] --> S1["by.id - 推薦"]
    Sel --> S2[by.text]
    Sel --> S3[by.label - accessibility]
    Sel --> S4[by.type - native type]
    Sel --> S5["by.traits - iOS only"]

    S1 --> Best[testID prop on RN element]
    S2 --> OK[文字 - 多語會跨]
    S3 --> A11y[最 accessible]

    style S1 fill:#10b981,color:#fff
```

**前端需要配合加 `testID`**：

```jsx
<TouchableOpacity testID="login-button" onPress={login}>
  <Text>登入</Text>
</TouchableOpacity>

<TextInput testID="email-input" value={email} onChangeText={setEmail} />
```

## Auto-sync — Detox 的殺手鐧

```mermaid
flowchart TD
    Action[element.tap] --> Wait[Detox 等]
    Wait --> Check{App 各 thread idle?}
    Check --> C1[JS thread]
    Check --> C2[Native UI thread]
    Check --> C3[Network requests]
    Check --> C4[Animations]
    Check --> C5[Timers]

    Check -->|全 idle| Go[執行 tap]
    Check -->|有忙| Wait

    style Go fill:#10b981,color:#fff
```

**不用 sleep、不用 waitFor**。Detox 自動等到 app idle 才動。

### 例外：手動 sync 控制

```javascript
// 暫停 sync（要測 loading state）
await device.disableSynchronization();
await element(by.id('submit')).tap();
await expect(element(by.id('spinner'))).toBeVisible();
await device.enableSynchronization();
```

## Mocking — Network / Module

### Mock 網路請求

```javascript
// 用 reactotron-mocks 或自寫 middleware
beforeEach(async () => {
  await device.launchApp({
    launchArgs: { MOCK_API: 'true' },
  });
});

// app 端
if (Config.MOCK_API) {
  api.interceptors.response.use(req => mockResponses[req.url]);
}
```

### Mock Native Module

```javascript
// e2e/mocks/Permissions.js
export default {
  request: () => Promise.resolve('granted'),
  check: () => Promise.resolve('granted'),
};
```

## Page Object Model 適配

```javascript
// e2e/pages/LoginPage.js
class LoginPage {
  emailInput = () => element(by.id('email-input'));
  passwordInput = () => element(by.id('password-input'));
  submitBtn = () => element(by.id('login-button'));
  errorText = () => element(by.text('帳號或密碼錯誤'));

  async login(email, password) {
    await this.emailInput().typeText(email);
    await this.passwordInput().typeText(password);
    await this.submitBtn().tap();
  }
}

export default new LoginPage();
```

```javascript
// e2e/login.test.js
import LoginPage from './pages/LoginPage';

it('login success', async () => {
  await LoginPage.login('qa@test.com', 'Test@123');
  await expect(element(by.text('Welcome'))).toBeVisible();
});
```

延伸：[Page Object Model 實戰](/automation/page-object-model.html)

## CI 整合 — GitHub Actions

### iOS（macOS runner）

```yaml
name: E2E iOS

on: [pull_request]

jobs:
  ios:
    runs-on: macos-14
    steps:
      - uses: actions/checkout@v4

      - uses: actions/setup-node@v4
        with: { node-version: 20 }

      - run: npm ci

      - name: Pod install
        run: cd ios && pod install

      - name: Build Detox
        run: detox build -c ios.sim.debug

      - name: Run Detox
        run: detox test -c ios.sim.debug --headless --record-logs all

      - if: failure()
        uses: actions/upload-artifact@v4
        with:
          name: detox-artifacts-ios
          path: artifacts/
```

### Android（Ubuntu runner）

```yaml
android:
  runs-on: ubuntu-latest
  steps:
    - uses: actions/checkout@v4
    - uses: actions/setup-node@v4
      with: { node-version: 20 }
    - uses: actions/setup-java@v4
      with: { distribution: 'temurin', java-version: '17' }

    - run: npm ci

    - name: AVD cache
      uses: actions/cache@v4
      with:
        path: |
          ~/.android/avd/*
          ~/.android/adb*
        key: avd-pixel-7-api-34

    - name: Build Detox
      run: detox build -c android.emu.debug

    - name: Run Detox in emulator
      uses: reactivecircus/android-emulator-runner@v2
      with:
        api-level: 34
        target: google_apis
        arch: x86_64
        profile: pixel_7
        script: detox test -c android.emu.debug --headless
```

## 跟 Firebase Test Lab 整合

Detox build 出來的 APK 可以上傳 FTL 跑跨真機：

```bash
# 1. Build instrumented APK + test APK
detox build -c android.emu.debug

# 2. 上傳 FTL
gcloud firebase test android run \
  --type instrumentation \
  --app android/app/build/outputs/apk/debug/app-debug.apk \
  --test android/app/build/outputs/apk/androidTest/debug/app-debug-androidTest.apk \
  --device model=Pixel7,version=34 \
  --device model=SamsungGalaxyS22,version=33
```

**local Detox 快 debug + FTL 跨真機驗證**。延伸：[Firebase Test Lab 完整指南](/automation/firebase-test-lab.html)

## 常見 7 個坑

### 1. iOS Simulator 抓不到 testID

**原因**：accessibility 沒 enable。
**解**：iOS Settings → Accessibility → 開 VoiceOver 一次（之後可關）。

### 2. typeText 中文字輸不進去

**原因**：Detox 用 native keyboard、不支援 IME。
**解**：用 `replaceText` 取代 `typeText`。

```javascript
await element(by.id('input')).replaceText('你好世界');
```

### 3. 動畫導致 sync timeout

**原因**：CSS animation 或 native animation 永遠不 idle。
**解**：手動 `device.disableSynchronization()` 或在 dev mode 關閉動畫。

### 4. WebView 內容測不到

**原因**：Detox 不直接支援 WebView。
**解**：用 web fallback 或 switch context（有限支援）。

### 5. CI emulator 啟動慢

**原因**：cold boot 每次 5+ 分鐘。
**解**：cache AVD（如上面 yml）+ 用 quick boot snapshot。

### 6. Flaky test 集中在某 modal

**原因**：modal animation 沒結束就互動。
**解**：用 `waitFor(element).toBeVisible().withTimeout(5000)` 顯式等。

### 7. Memory leak 跑 100 個 test 後 OOM

**原因**：每 test 沒 reload。
**解**：`beforeEach: await device.reloadReactNative();`

## Detox vs Appium 對比

| 維度 | Detox | Appium |
|------|-------|--------|
| 速度 | ⚡ 快 5-10x | 慢 |
| 支援框架 | RN（主）/ Native | 任何 |
| 跨平台 code | 同一份 | 同一份（但細節差） |
| Auto-sync | ✓ 內建 | ✗ 要手動 wait |
| 真機 | iOS 部分、Android 需設定 | 完整 |
| WebView | 弱 | 強 |
| 學習曲線 | 中 | 高（環境設定） |
| 社群 | 中（RN 用戶） | 大（業界標準） |

延伸：[Mobile App Testing 入門（Appium vs Detox）](/automation/mobile-testing-appium-detox.html)

## 反模式

```mermaid
flowchart TD
    Anti[Detox 反模式] --> A1["每 test 不 reload → state 污染"]
    Anti --> A2["大量 typeText 中文 → IME 衝突"]
    Anti --> A3["未配合 dev 加 testID"]
    Anti --> A4["Sync 不關就測 loading"]
    Anti --> A5["跑 prod build (沒 debug symbols)"]
    Anti --> A6["CI 不 cache AVD"]
    Anti --> A7["真機跑 Detox（部分不支援）"]

    style A1 fill:#ef4444,color:#fff
    style A2 fill:#ef4444,color:#fff
    style A3 fill:#ef4444,color:#fff
    style A4 fill:#ef4444,color:#fff
    style A5 fill:#ef4444,color:#fff
    style A6 fill:#ef4444,color:#fff
    style A7 fill:#ef4444,color:#fff
```

## 給 RN QA 的 5 句

1. **RN 就選 Detox、別碰 Appium 浪費生命**
2. **跟 dev 配合 testID = 一勞永逸**
3. **Sync 是神功、別輕易關**
4. **Local Detox + FTL = 完整 Android 覆蓋**
5. **CI 一定 cache AVD、不然每次 5 分鐘 boot**

## 最後

Detox 是 React Native QA 的**最佳投資**。從 setup 到第一個綠 CI 30 分鐘、之後每個 test 比 Appium 省 70% 時間。一年累積省下的時間夠你深入學 5 個新主題。

延伸：
- [Mobile App Testing 入門（Appium vs Detox）](/automation/mobile-testing-appium-detox.html)
- [Firebase Test Lab 完整指南](/automation/firebase-test-lab.html)
- [Page Object Model 實戰](/automation/page-object-model.html)
