> 九號工具站
返回列表

Detox 完整入門 — React Native 自動化神器 從 0 到 CI 整合

Detox 完整指南。React Native E2E 框架、灰盒測試原理、Sync 機制、Page Object 適配、Mocking、CI 整合(GitHub Actions + Firebase Test Lab)、跟 Appium 對比。

QA detox react-native e2e mobile-automation gray-box
· 最後更新 2026-06-21 · 最後審閱 2026-08-30

💡 本文原刊於 qa.9niche.com,2026-08 併入 9niche.com 懶人包,內容照原文完整搬遷。

1. 前言

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

2. 為什麼 Detox 比 Appium 快

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

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

3. 30 分鐘 0 到綠 CI

Setup

# 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 配置

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

// 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('[email protected]');
    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('[email protected]');
    await element(by.id('password-input')).typeText('wrong');
    await element(by.id('login-button')).tap();
    await expect(element(by.text('帳號或密碼錯誤'))).toBeVisible();
  });
});

# 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

4. Selector 哲學

前端需要配合加 testID

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

<TextInput testID="email-input" value={email} onChangeText={setEmail} />
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

5. Auto-sync — Detox 的殺手鐧

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

例外:手動 sync 控制

// 暫停 sync(要測 loading state)
await device.disableSynchronization();
await element(by.id('submit')).tap();
await expect(element(by.id('spinner'))).toBeVisible();
await device.enableSynchronization();
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

6. Mocking — Network / Module

Mock 網路請求

// 用 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

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

7. Page Object Model 適配

// 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();
// e2e/login.test.js
import LoginPage from './pages/LoginPage';

it('login success', async () => {
  await LoginPage.login('[email protected]', 'Test@123');
  await expect(element(by.text('Welcome'))).toBeVisible();
});

延伸:Page Object Model 實戰

8. CI 整合 — GitHub Actions

iOS(macOS runner)

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)

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

9. 跟 Firebase Test Lab 整合

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

# 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 完整指南

10. 常見 7 個坑

1. iOS Simulator 抓不到 testID

原因:accessibility 沒 enable。 :iOS Settings → Accessibility → 開 VoiceOver 一次(之後可關)。

2. typeText 中文字輸不進去

原因:Detox 用 native keyboard、不支援 IME。 :用 replaceText 取代 typeText

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();

11. Detox vs Appium 對比

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

延伸:Mobile App Testing 入門(Appium vs Detox)

12. 反模式

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

13. 給 RN QA 的 5 句

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

14. 最後

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

延伸:

ℹ️

一般聲明

本站提供之資訊僅供參考,不保證其完整性與正確性。使用者應自行判斷資訊之適用性。

意見反饋