ハコヅミを作る #28:10分かかっていた E2E を1分半にし、実装をバックグラウンドに任せる

前回(#27)でログインが入り、ステップ2 が始まりました。ステップ2 は、Issue の数が多くなりそうです。

始める前に、気になっていたことを片づけました。テストの待ち時間です。

  • E2E の全件を、約9分半から1分半にする(#136)
  • 手元で流すテストの決まりを変える
  • 実装をバックグラウンドに任せて、その間に次の Issue を相談する

1つの Issue で、20分待っていた

ハコヅミでは、Claude Code が PR を作る前に、手元でテストを全部流す決まりにしていました。E2E は 223 件あり、全件を流すと約9分半かかります。そのあと PR を作ると、CI でもう一度、同じ E2E が流れます。

#17で、CI の E2E は3つに分けて同時に流すようにしました。それでも、いちばん長いものは8分から13分かかっていました。合わせると、1つの Issue ごとに20分ほど、テストを待っていたことになります。

Claude Code に、減らす方法を聞きました。案は2つでした。

  1. テストそのものを速くする
  2. 手元では、関係するテストだけを流す(全件は CI に任せる)

両方やることにして、1つの Issue にまとめました。


遅さの正体は、コマンドの起動だった

E2E には、画面からは確かめられないことを、DB を直接見て確かめるテストがあります。「ほかの人のデータが変わっていない」「画像が R2 から消えている」などです。

これを、wrangler のコマンドを起動して行っていました。

// 前の作り(イメージ):D1 を見るたびに、コマンドを1つ起動する
execFileSync('npx', ['wrangler', 'd1', 'execute', 'DB', '--local', '--command', sql]);

測ってみると、起動だけで1回に約1.6秒かかっていました。テストのコードの中に、この呼び出しが 112 か所あります。テストのデータを入れるのにも使っているので、1つのテストで何回も呼ばれます。

ローカルの D1 は、ただの SQLite のファイル

wrangler dev で動かすローカルの D1 は、中身が SQLite のファイルです。.wrangler/state/v3/ の下に置かれています。R2 も同じで、オブジェクトの一覧を SQLite に持っています。

Node 24 には、SQLite を開く仕組み(node:sqlite)が入っています。これで、ファイルを直接開くようにしました。パッケージは足していません。

import { DatabaseSync } from 'node:sqlite';

const db = new DatabaseSync(path);
db.exec('PRAGMA busy_timeout = 5000'); // ロックが外れるのを待つ
const rows = db.prepare(sql).all();
db.close();

テストから呼ぶ関数(queryLocalD1() など)の形は変えていないので、今までのテストは1行も直していません。

結果

前 後
手元の全件 約9分30秒 1分24秒〜1分48秒
CI の E2E(3つのうち、いちばん長いもの) 8分〜13分 2分半〜3分

アプリが動いている最中に、同じファイルを別のところから開くことになります。ロックで落ちないかが心配だったので、全件を3回続けて流して、落ちないことを確かめました。

気をつけること

この方法は、wrangler(の中の miniflare)が、ファイルをどこにどう置くかに頼っています。wrangler を上げて置き方が変わると、テストが「ファイルが見つからない」で落ちます。開く場所は1つのファイルにまとめてあるので、そのときは1か所を直します。

node:sqlite も、Node の資料では、まだ開発が続いている機能の扱いです。使うのは「開く・実行する・閉じる」だけで、テストの中だけなので、変わっても直しやすいと考えました。


手元では、関係するテストだけを流す

もう1つは、決まりの変更です。CLAUDE.md と、/implement のスキルを直しました。

  • 手元では、型チェック・Lint・ユニットテストと、変えたところに関係する E2E を流す
  • E2E の全件は、PR の CI に任せる
  • 全部の画面に効く変更(レイアウト、ヘッダー、ログイン、共通の部品、テストの仕組み)のときは、手元でも全件を流す

PR の本文には、手元で流した E2E がどれかを書いてもらいます。レビューするときに、「全件は CI で見る」と分かるようにするためです。


速くなったら、別の問題が出た

テストが速くなって、気軽に何度も流せるようになりました。すると、手元の D1 にテストのデータがたまるのも速くなりました。

E2E は、開発サーバーと同じローカルの D1 を使い、データを消しません。全件を5回流したところで、所有のギアが 1,000 件を超えました。ギアの一覧のタブに出る件数が4桁になり、「幅 375px ではみ出さない」のテストが2件落ちます。

テストが悪いのではなく、データが多すぎるだけです。手元の D1 を消して作り直せば、通ります。このあとも何度か同じことが起きたので、作り直すコマンドを用意するのが、次の宿題です。


実装を、バックグラウンドに任せる

#136 の実装を待っている間に、聞いてみました。実装を流しながら、次の Issue の相談はできるのか、と。

できました。Claude Code は、作業を別のエージェントに任せて、バックグラウンドで動かせます。ステップ2 の最初の2つ(管理者、上限)は、この形で進めました。

  1. /implement 138 を、バックグラウンドで始めてもらう
  2. その間に、次の Issue(上限、アカウントの画面、エクスポート、退会)の下書きを相談して、作る
  3. 実装が終わると、報告が届く。PR を見て、マージする

任せるときは、Claude Code が、エージェントへの指示をまとめて書きます。読むファイル、守る決まり(本番に触らない、マージしない)、設計の方針、テストで気をつけること、最後に報告してほしいこと、などです。

任せた PR で、CI が落ちた

1つ目の PR は、CI の E2E が1件落ちました。キャンプ場の引用のテストで、今回の変更とは関係のないところです。

調べてもらうと、原因はテストの順番への頼りでした。キャンプの登録画面は、プランが1つもないと入力欄を出しません。落ちたテストは、自分ではプランを作らず、ほかのテストが先に作ったプランに頼っていました。

CI では、E2E を3つに分けて、それぞれまっさらな D1 から流します。今回テストが増えて、分け目が動きました。その結果、頼っていたテストが、まっさらな D1 で最初に流れて、落ちたわけです。

同じ頼り方をしていたテストが、ほかに3つ見つかりました。4つとも、自分でプランを入れるように直しています。次の PR からは、作る前に、まっさらな D1 で3つの分け目を流して確かめてもらうようにしました。

使ってみて

  • 並行して進むのは、たしかに速い。1つ目の実装を待つ20分ほどで、Issue を3つ作れた
  • 報告をうのみにしない。PR の要のファイル(マイグレーション、権限の判定)は、マージの前に読む
  • 同じファイルを触る Issue は、並べられない。管理者の次の「上限」は、同じテーブルと同じファイルを触るので、前の PR のマージを待った
  • 後半の3つ(アカウントの画面、エクスポート、退会)は、ふつうに、会話の中で実装してもらった。途中で画面を撮って見せてもらえるので、見た目のある変更は、こちらのほうが合っていた

おわりに

  • 遅いテストは、まず「何に時間がかかっているか」を測る。今回は、テストの中身ではなく、コマンドの起動だった
  • ローカルの D1 は SQLite のファイル。Node から直接開ける
  • 手元では関係するテストだけ、全件は CI。役割を分ける
  • テストは、ほかのテストが作ったデータに頼らない。CI の分け方が変わると、落ちる
  • 実装をバックグラウンドに任せると、その間に次のことを考えられる。ただし、PR は自分で読む

次回は、ステップ2 の本題に入ります。管理者の権限と、登録数の上限です。

← トップページに戻る