RORK LABEN
MAX — Rork MaxはClaude CodeとClaude Opus 4.6を基盤に、React NativeではなくネイティブSwiftアプリを直接生成しますAPPLE — Rork MaxはiPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageまでAppleエコシステム全体を対象にしますWORKFLOW — 実運用ではAIに足場を作らせ、状態管理とデータ層は自分で書き直す分担に落ち着いてきたという声が目立ちますSEED — 4月にLeft Lane Capital主導で$15Mのシードを調達しました(Peak XV・True Ventures・a16z Speedrunが参加)PAPERLINE — アプリビルダーPaperlineを買収。エンジニア人材の獲得へ今後も買収を続ける方針ですREVIEW — 3ヶ月使ったユーザーの再訪レビューが増え、得意・不得意と向く用途の見極めが進んでいますMAX — Rork MaxはClaude CodeとClaude Opus 4.6を基盤に、React NativeではなくネイティブSwiftアプリを直接生成しますAPPLE — Rork MaxはiPhone・iPad・Apple Watch・Apple TV・Vision Pro・iMessageまでAppleエコシステム全体を対象にしますWORKFLOW — 実運用ではAIに足場を作らせ、状態管理とデータ層は自分で書き直す分担に落ち着いてきたという声が目立ちますSEED — 4月にLeft Lane Capital主導で$15Mのシードを調達しました(Peak XV・True Ventures・a16z Speedrunが参加)PAPERLINE — アプリビルダーPaperlineを買収。エンジニア人材の獲得へ今後も買収を続ける方針ですREVIEW — 3ヶ月使ったユーザーの再訪レビューが増え、得意・不得意と向く用途の見極めが進んでいます
記事一覧/開発ツール
開発ツール/2026-07-13上級

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

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

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-07-01
Rork Max のクラウドコンパイル — Mac を持たずにネイティブアプリを仕上げる
Rork Max のクラウドコンパイルで、ローカル Mac なしに iOS/iPadOS のネイティブアプリをビルド・実機テスト・公開する流れを、失敗時の切り分けと実機 Mac を残す判断基準まで含めて実務目線でまとめます。
開発ツール2026-06-17
誕生日を集めずに年齢を確認する — Declared Age Range API を Rork アプリに組み込む
iOS 26 の Declared Age Range API を使い、生年月日を保存せずに年齢層だけを受け取る実装方法。Rork Max のネイティブ Swift と、標準 Rork(Expo)のネイティブモジュール越しの呼び出しを、責務の線引きとともに整理します。
開発ツール2026-05-25
Rork で作る iOS アプリに StoreKit 2 で買い切り課金を実装するノート
Rork が生成する Swift / SwiftUI コードに StoreKit 2 で買い切り課金を後付け実装する手順メモ。累計 5,000 万 DL の壁紙アプリで StoreKit 1 から 2 へ移行した経験を踏まえて、ProductID 設計・Transaction 検証・Paywall UI・本番ハマりどころまで実装サンプル込みで共有します。
📚RECOMMENDED BOOKS
大規模言語モデル入門
山田育矢
LLM開発
生成AIプロンプトエンジニアリング入門
我妻幸長
プロンプト
Claude CodeによるAI駆動開発入門
平川知秀
AI駆動開発
※ アフィリエイトリンクを含みます
もっと見る →