◉RORK LABEN
●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…●GPT-6.1 SOL — Rork のモデルメニューに追加(09-29)。Low/Medium/High を選べ、Pro・Max に含まれます●SONNET 5.5 — UI づくりが得意で、Sonnet 5 より出力が速いモデルが選べます(09-28)●SDK 58 — Beta が 09/15 から。stable の案内はまだなく、Expo Go でのプロジェクト実行は 09/03 からログインが必要です●WIDGET — iOS のウィジェットが本番ビルドだけ真っ黒になる原因を追った報告が出ています●OBSERVE — EAS Observe がネイティブクラッシュも記録します(SDK 57.0.21 以降・10/07)●NEW — 起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、…
記事一覧/開発ツール
⬡ 開発ツール/2026-07-13上級

Detox と Maestro の E2E が全部緑なのに本番でクラッシュが出ていたとき — 自動リトライが握りつぶした不安定テストをフレーク率で炙り出す運用メモ

DetoxとMaestroのE2Eが全部緑なのに本番でクラッシュが出た原因は、自動リトライがフレークを握りつぶしていたことでした。JUnit XMLを試行単位で読んでテスト別フレーク率を計測し、隔離とリトライ予算で本物の失敗だけを残す運用を記録します。

rork-max41e2e-testingdetoxmaestroci-cdflaky-testquality-assurance

✦ プレミアム記事

ある朝、CI のチェックはすべて緑でした。E2E スイートも 42 本すべて pass。そのまま OTA を配信した数時間後、決済(Stripe 連携)完了画面でのクラッシュレポートが届きます。しかもその動線は、E2E で毎回テストしているはずの流れでした。

緑だったのに、通っていなかった。

ログを遡って理由が分かったとき、正直なところ胸のあたりが重くなりました。原因はテストコードのバグでも、アプリの新しい不具合でもありません。自動リトライが、不安定なテストの赤を静かに握りつぶしていたのです。決済フローのテストは3回に1回ほど失敗していて、CI はそれを3回まで再実行し、たまたま通った1回を「緑」として記録し続けていました。

このメモは、その「信用できない緑」に気づき、フレーク(不安定なテスト)を計測して隔離し、本物の失敗だけをリリースの歯止めに残すまでの運用記録です。Rork や Rork Max で生成したアプリに Detox / Maestro の E2E を載せている個人開発者向けに、私自身が手元のアプリ群で踏んだ順番のまま書いています。

緑が緑でなくなる仕組み — リトライは失敗を消しゴムにする

多くの E2E ランナーは、不安定さへの対処として自動リトライを備えています。Detox の Jest 連携なら jest.retryTimes(2)、Maestro なら CI 側で retry を回す構成が定番です。

リトライ自体は悪ではありません。実機・エミュレータの E2E は、アニメーションの完了待ちやネットワークの揺らぎで、コードが正しくても落ちることがあります。問題は、リトライの結果を「最終的に通ったか」だけで記録してしまうことにあります。

3回リトライして1回通れば緑。この記録方法だと、次の2つが区別できません。

状態3回の結果記録本当の意味
安定して健全○ ○ ○緑問題なし
フレーク(不安定)× × ○緑いつ本番で落ちてもおかしくない

下の行が落とし穴です。表向きは緑なので誰も気づきません。けれど失敗率 67% のテストは、テスト環境より条件の悪い実機(App Store 配信版)で、いつか本物の赤に変わります。私の決済フローがまさにこれでした。

大事な発想の転換は、合否を二値でなく「試行の履歴」として持つことです。最終結果だけを見るのをやめ、リトライを跨いだ各試行の○×を残せば、フレークは数字になって見えてきます。

フレーク率を計測する — JUnit XML を試行単位で読む

Detox(Jest)も Maestro も、JUnit 形式の XML を吐けます。ここに各テストの <testcase> と、失敗時の <failure> が入ります。リトライを別々の実行として出力させれば、試行の履歴を機械的に集計できます。

まず、リトライを「上書き」でなく「追記」で残す設定にします。GitHub Actions で Maestro を回す場合、リトライごとにレポートを別ファイルへ出すのが確実です。

# .github/workflows/e2e.yml(抜粋)
- name: Run Maestro E2E (3 attempts, keep every report)
  run: |
    for attempt in 1 2 3; do
      maestro test .maestro/ \
        --format junit \
        --output "reports/e2e-attempt-${attempt}.xml" || true
    done
  # || true で途中失敗しても全試行を回し切る。合否判定は後段の集計に委ねる

ポイントは || true です。ここで失敗即中断にすると、フレークの「×」が記録される前にジョブが止まってしまいます。判定をランナーから引き剥がし、集計スクリプトへ移すのが要点です。

次に、複数の XML を読んでテスト単位のフレーク率を出す Node スクリプトです。依存は軽量な XML パーサ1つだけにしています。

// scripts/flake-rate.mjs
import { readdirSync, readFileSync } from "node:fs";
import { XMLParser } from "fast-xml-parser";
 
const DIR = "reports";
const parser = new XMLParser({ ignoreAttributes: false, attributeNamePrefix: "@_" });
 
// テスト名 -> { runs: 総試行数, fails: 失敗回数 }
const stats = new Map();
 
for (const file of readdirSync(DIR).filter((f) => f.endsWith(".xml"))) {
  const xml = parser.parse(readFileSync(`${DIR}/${file}`, "utf8"));
  const suites = [].concat(xml.testsuites?.testsuite ?? xml.testsuite ?? []);
  for (const suite of suites) {
    for (const tc of [].concat(suite.testcase ?? [])) {
      const name = `${tc["@_classname"] ?? ""} › ${tc["@_name"]}`;
      const rec = stats.get(name) ?? { runs: 0, fails: 0 };
      rec.runs += 1;
      if (tc.failure || tc.error) rec.fails += 1; // 1試行でも赤なら失敗として計上
      stats.set(name, rec);
    }
  }
}
 
const rows = [...stats.entries()]
  .map(([name, { runs, fails }]) => ({ name, runs, fails, rate: fails / runs }))
  .filter((r) => r.fails > 0 && r.fails < r.runs) // 全部通る/全部落ちるは除外=フレークのみ
  .sort((a, b) => b.rate - a.rate);
 
for (const r of rows) {
  console.log(`${(r.rate * 100).toFixed(0).padStart(3)}%  ${r.fails}/${r.runs}  ${r.name}`);
}
process.exitCode = 0; // 計測は常に成功で返す。ゲート判定は別ジョブが担う

r.fails > 0 && r.fails < r.runs の条件が肝心です。全試行で落ちるものは本物の失敗(別で赤にすべき)、全試行で通るものは健全。その中間、つまり「通ったり落ちたりする」ものだけがフレークです。ここを取り違えると、本物のバグをフレーク扱いで見逃します。

手元の6アプリで初めて回したとき、フレーク率トップは決済フローの 67%(3試行中2失敗)でした。次点がプッシュ通知の許諾ダイアログ待ちで 40% ほど。数字にして初めて、握りつぶされていた不安定さの輪郭が見えました。

✦

ここまでお読みいただきありがとうございます。

この記事の続きを読む

この先には、実装コードやベンチマーク結果など、実務でお役に立てる内容をご用意しています。このサイトは広告を掲載しておらず、サーバーや開発にかかる費用はメンバーの皆様のご支援で成り立っています。もしお役に立てていましたら、ご支援いただけますと大変ありがたいです。

この記事で得られること
✦リトライ跨ぎの合否を JUnit XML から拾い、テスト単位でフレーク率を出す Node スクリプト
✦単一テスト20%以上で隔離する quarantine 運用と、1ビルド3回までのリトライ予算での歯止め
✦緑が信用できなくなる前に気づくための、CI ゲートと週次レビューの落としどころ
Stripe による安全な決済 · いつでもキャンセル可能
✦

この記事を購入する

この先の内容をすべてお読みいただけます。一度のご購入で、いつでも何度でもアクセスできます。このサイトは広告を掲載しておらず、皆さまのご支援がサーバー費用などの運営を支えています。

または
メンバーシップなら全記事が読み放題 →
シェア

お読みいただきありがとうございます

Rork Lab は広告なしで運営しており、サーバー費用などの運営コストはメンバーシップのご支援で賄っています。実装コード・ベンチマーク・本番設計パターンなど、実務でお役立ていただける記事を毎日更新しています。もし読んでよかったと感じていただけましたら、ぜひご覧ください。

  • ✦コピー&ペーストで使える実装コード付き
  • ✦毎日新しい上級ガイドを追加
  • ✦¥580/月 または ¥1,480 の永久アクセス
メンバーシップを見る →

関連記事

⬡ 開発ツール2026-10-15
EXPO_PUBLIC_ の値が、開発では読めて本番だけ undefined になる書き方を、変換結果で見分けます
API の URL が開発中は読めるのに、ビルドした途端に undefined になる。原因は process.env の書き方でした。babel-preset-expo の変換結果を開発・本番で並べ、置き換わる書き方と残ってしまう書き方を表にしました。
⬡ 開発ツール2026-10-14
起動広告が、背面から戻った回にしか出ていませんでした。「前面に戻った」合図が、読み込みより先に過ぎていたためです
Android 版の起動広告が、アプリを完全に終了してから開く回だけ一度も出ていませんでした。「前面に戻った」合図を読み込みの完了と切り離し、順番ごとにテストできる形へ組み直した記録です。
⬡ 開発ツール2026-10-13
リリース前に、書き出した Expo バンドルの中から自分の鍵を探してみる
ソースに鍵がなくても、出来上がったアプリの中に鍵が残ることがあります。Expo SDK 57 で実際に expo export を実行し、バンドルの中身を grep と 60 行ほどの Node スクリプトで調べて、見つかる鍵と見つからない鍵の違いを確かめました。