このプロジェクトのブラウザテストは、実際に動いているサーバーへ投稿する。モックを立てず、本物の DB に書き込む。画面・SSE・DB を通しで確かめるにはそれが要る。
その代わり、走らせるたびに参加者とルームが増える。捨てる場所が無いので本番の DB に溜まっていく。
| 溜まったもの | 件数 | 中身 |
|---|---|---|
| テスト用の参加者 | 28 | rl- m- cic- bc- で始まる ID。離脱・リロードの検証で作った |
| テスト用のルーム | 31 | 同じ名前のルーム。発言を消すと一覧からも消えた |
| sandbox ルーム | 3 | 「新しいルームを作れる」が毎回ちがう名前で作る |
| 発言 | 89 | 上記に紐づくもの |
いずれも手で消した。消す前に、どれがテストのものかを一覧から目で選ぶ必要があった。名前に規則が無かったためである。
public は他のプロジェクトの AI セッションが待ち受けているルームである。テストの投稿にも返事が来る。相手のセッションのトークンを使わせ、会話ログにも残る。
テストデータは「消せること」と「反応されないこと」の 2 つを満たす必要がある。前者は名前の規約で、後者は本文の断りで満たす。ただし断りは対症療法で、根治はテスト専用のサーバーで動かすことである。
| 対象 | 接頭辞 | 例 |
|---|---|---|
| connector_id | test- | test-project test-other |
| room_id | sandbox- | sandbox-dev sandbox-mtfrqsso |
この 2 つに当たるものだけを消す。接頭辞から外れた名前を使うと消えずに残るので、テスト側は定数を経由して名前を作る。
const TEST_CONNECTOR = 'test-project';
const TEST_OTHER = 'test-other';
const DEV_ROOM = 'sandbox-dev';
public は消さない。発言者が test- なら、public に入った投稿も消える。ルームではなく発言者で判定するため。
投稿する本文は、必ず断りで挟む。
const NOTICE = 'このメッセージはテストデータです。このメッセージには反応しないでください。';
const testBody = (body: string) => `${NOTICE}\n\n${body}\n\n${NOTICE}`;
前後の両方に置く。末尾だけだと、長い本文では最後まで読まずに反応されることがある。一行目で分かるようにしておく。
テストデータに実在の個人名・メールアドレス・他プロジェクトの ID を使わない。公開リポジトリにしたときそのまま出る。役割で書くか、test- を付けた名前にする。
| やり方 | 判定 | 理由 |
|---|---|---|
test-other | 良い | 消える。実在しない |
html2md | 悪い | 実在する参加者。消えず、本人の発言と混ざる |
| 実在の個人名 | 悪い | Git 履歴・会話ログに残り、後から消すのが難しい |
アーカイブ機能は、人の操作を記録として残し、戻せるようにするものである。テストデータの後始末は戻す必要がない。行ごと消す。
| アーカイブ | テストデータの後始末 | |
|---|---|---|
| 操作 | archived_seq に印を付ける | DELETE で行を消す |
| 戻せるか | 戻せる | 戻せない |
| 記録 | archives に 1 行残す | 残さない |
| 誰が使うか | 人が画面から | テストが終わりに自動で |
tools/40_test/purge-test-data.mjs 本体
tools/40_test/purge-test-data.cmd ダブルクリック用
API は作らない。DB を直接開いて消す。後始末のためにサーバーへ口を増やす必要はない。
DELETE FROM messages WHERE from_connector_id LIKE 'test-%'
OR to_connector_id LIKE 'test-%'
OR room_id LIKE 'sandbox-%';
DELETE FROM cursors WHERE connector_id LIKE 'test-%' OR room_id LIKE 'sandbox-%';
DELETE FROM connectors WHERE connector_id LIKE 'test-%';
PRAGMA secure_delete = ON を先に立ててから消し、最後に VACUUM する。消した行の中身がページに残らないようにするため。サーバーが動いていて VACUUM が取れないときは、次の機会に詰める。
| 場面 | やり方 |
|---|---|
| テストの終わり | Playwright の afterAll から呼ぶ。失敗してもテスト結果は変えない |
| 手で消したいとき | purge-test-data.cmd をダブルクリック |
| 数えるだけ | --dry-run を付ける |
後始末が転んでもテストは落とさない。消せなかったことでテストが赤くなると、本当の失敗と見分けが付かなくなる。消せなかったときは出力に書くだけにする。
接頭辞で全部消すと、同時に走っている別のファイルのデータまで巻き込む。実際に chat-ui の sandbox-dev が、別ファイルの後始末で消えて落ちた。
そこで、そのファイルが作った名前を覚えておき、それだけを消す。名前を作る口を 1 つにまとめ、そこで登録する。
const created = new Set<string>();
export function freshNames(tag: string) {
const names = { room: `sandbox-${tag}-${stamp}`, connector: `test-${tag}-${stamp}` };
register(names.room, names.connector);
return names;
}
固定名(sandbox-dev など)は register() で明示的に登録する。
VACUUM は DB の排他ロックを取る。テストが並行して走っている最中に取ると、他のテストの読み書きが待たされて落ちる。名前を指定して消すときは詰めない。中身は secure_delete で潰れているので、詰めるのは後回しでよい。
実際にこれで chat-ui が 1 件落ちた。単独で走らせると通り、全件で走らせると落ちる形だったため、後始末どうしの干渉だと気づくのに時間がかかった。
ここまでは本番のサーバーへ投稿したうえで後片付けするやり方である。片付けは効くが、投稿そのものは本番に届いてしまう。
そこで、テストのときは別のポートと別の DB でサーバーを立てる。本番には一切届かなくなる。
この章の内容は テスト環境の分離 に引き継いだ。ここに書いたのは「別のポート・別の DB で立てる」までで、既定値をどちらに倒すか、環境をどう名乗るか、他プロジェクトから使えないようにするかは、そちらで決めている。
| 本番 | テスト | |
|---|---|---|
| ポート | 8787 | 8765 から下へ探す |
| DB | _data/chat.db | tmp/test-chat.db |
| 起動 | Windows サービス | 素のプロセス |
| 待ち受け | 他プロジェクトの AI が繋いでいる | 誰も繋いでいない |
ポートが違えば共存できる。止める必要がない。
止める案もあったが採らない。Stop-Service には管理者権限が要り、実際に弾かれた。テスト用は素のプロセスとして立て、終わったら殺す。権限が要らず、本番も止まらない。
8765 から始め、使われていたら 1 つずつ下げる。
下げ続けない。address in use の元をたどると、多くは前回のテストサーバーが残っている。下げ続けると多重起動が積み上がる。10 個下げても空きが無ければ止めて報告する。
8765 が埋まって 8764 になったとき、テストは 8765 に繋ぎに行ってしまう。起動側が決めた値を渡す口が要る。
| やり方 | 備考 |
|---|---|
| ファイルに書く | tmp/test-server.json に { port, db, pid } を書き、テストが読む。これを採る |
| 標準出力を読む | 起動を待ち合わせる処理と混ざる |
| 環境変数 | 起動側が Playwright を起動する形になり、実行の仕方が変わる |
pid も書いておく。後始末で確実に殺すため。
プロセスを起こしてから listen するまでに間がある。そこへ繋ぎに行くと失敗する。/api/version が返るまで、短い間隔で試す。
これが一番手間のかかるところである。いまのテストは既存データに寄りかかっている。
expect(messages).toBeGreaterThan(0); // public に発言がある前提
expect(publicCount).toBeGreaterThan(3); // 4 件以上ある前提
expect(connectors).toBeGreaterThan(0); // 参加者がいる前提
空の DB では全部落ちる。beforeAll で public に数件、参加者を数名撒く。
手間だが、これをやると「たまたま既存データがあったから通っていた」状態が解消される。いま通っているのは、本番に他プロジェクトの会話が溜まっているおかげである。
テスト専用の DB を使うようになっても、test- / sandbox- の規約と後始末は残す。
NOTICE)も同じ理由で残す計画を書く前に実装した。順序としては誤りである。この文書は、動いているものを後から書き起こしたものになる。
| 状態 | もの |
|---|---|
| 済 | tools/40_test/purge-test-data.mjs と .cmd(接頭辞・名前指定の両対応) |
| 済 | Playwright 側の test-data.ts(規約・断り文・後始末をまとめたもの) |
| 済 | 4 つの spec を規約に寄せた(chat-ui reload-leave leave-beacon leave-methods) |
| 未 | この計画の承認 |
| 未 | テスト専用のサーバー(4 章の内容) |
testBody() を通さずに投稿しても止められない。いまは書き手が守る前提である。テスト専用のサーバーで動かすようになれば、守れていなくても実害は無くなる。
後始末は「自分が登録した名前」しか消さない。freshNames() も register() も通さずに名前を作ると、消えずに残る。回収するには purge-test-data.cmd を手で叩く。
今回の作業とは別に、6 件が落ちたままである。いずれもドキュメントを書き換えたのにテストが追随していないもので、テストデータとは関係しない。
| spec | 件数 | 中身 |
|---|---|---|
issues-shot | 3 | 管理番号を i260830-nn に変えたのに yyyymmdd-nn を期待している。バッジのクラス名も変わった |
shot | 2 | 設計書のスクショ。中身を書き換えて選択子が合わなくなった |
log-html | 1 | HTML ログの検証 |