本番環境のDBを消し飛ばしたあの日から、俺がPlaywrightと心中するまで

本番環境のDBを消し飛ばしたあの日から、俺がPlaywrightと心中するまで

「本番環境のDBをテストで消し飛ばした」。

そんな悪夢のような朝から、俺のPlaywrightとの格闘は始まった。自動テストを導入して「これで安心だぜ!」とイキっていたのも束の間、CIで回すたびに落ちるテスト、夜中に鳴り響くアラート通知。あの時の俺は、テストコードを「資産」と呼んでいたが、現実はただの「呪い」を量産していただけだった。

なぜ、俺たちは自動テストに裏切られるのか。その答えは、技術の未熟さではなく、「テストをプロダクトの一部として愛せていなかった」という傲慢さにあった。


1. セレクタへの過信は「死」を招く

自動テストを始めた当初、俺はChromeのデベロッパーツールで「Copy Selector」を連打していた。`#app > div > div:nth-child(3) > button`。こんなクソみたいな文字列をテストに埋め込んで、「よし、自動化完了!」と胸を張っていたんだ。

結果は言うまでもない。UIの微調整でテストは全滅。「またお前か!」と叫びながら、毎日数時間をテストの修正に費やすハメになった。UI構造に依存したセレクタは、「壊れるために書いているコード」だ。

結論:`data-testid` 以外は信じるな

UIの構造変更に強いテストを書く唯一の解は、テスト専用の属性をDOMに埋め込むことだ。

// 悪い例:構造が変わるたびにテストが死ぬ
await page.click('#app > div > div:nth-child(3) > button');

// 良い例:テスト専用の属性を定義する
// <button data-testid="submit-order-button">注文確定</button>
await page.getByTestId('submit-order-button').click();


2. Flaky(不安定)なテストという名の呪い

「ローカルでは通るのに、CIだと落ちる」。これがエンジニアの精神を最も削る。
最初はサーバーのスペックやネットワークのせいにしていたが、原因は単純。「期待する状態」を定義せず、「時間」に頼っていたからだ。

`page.waitForTimeout(3000)`。これを使った瞬間に、そのテストはゴミになる。ボタンが表示されていても、JSのイベントリスナーがバインドされる前ならクリックは空振りする。Playwrightは優秀だが、魔法ではない。「DOMに存在すること」と「操作可能であること」は別物だ。

# 悪い例:神頼みの待機(3秒待てば通るだろうという怠惰)
import time
time.sleep(3) 

# 良い例:ステートベースの待機(期待する状態になるまで待つ)
from playwright.sync_api import expect

button = page.get_by_role("button", name="保存")
# ボタンが有効化されるまで自動で待機する
expect(button).to_be_enabled()
button.click()


3. CI/CDの並列化が引き起こした「DB競合」の悲劇

テストが30分もかかると、誰も結果を見なくなる。俺はGitHub Actionsの `shard` を使い、テストを3分割して並列実行することで、実行時間を15分から3分まで短縮した。

# .github/workflows/playwright.yml
strategy:
  matrix:
    shard: [1/3, 2/3, 3/3]
steps:
  - run: npx playwright test --shard=${{ matrix.shard }}

だが、ここで地獄を見た。並列化するとDBが共有され、ユーザー登録テストと削除テストが衝突して全滅したんだ。

解決策は「テストの完全な独立」だ。
俺はテスト実行ごとにUUIDを付与した一時的なDBスキーマを作成し、テスト終了後に破棄する設計に切り替えた。

  • テスト用DBの生成: `docker-compose` でテスト専用のコンテナを立ち上げる。
  • UUIDによる分離: ユーザー名に `test_user_${uuid.uuid4()}` を使用し、衝突を物理的に排除する。
「面倒くさい」と感じるかもしれないが、この分離をサボると、並列化はただの「ガチャ」になる。

4. テストは「書く」より「捨てる」勇気が大事

全画面の全ボタンを網羅しようとして大火傷した経験から、今の俺には明確な境界線がある。

テストの種類役割境界線
ユニットテストロジックの正当性計算処理、バリデーション
E2Eテストユーザー体験の保証ログイン、決済、データ保存などの重要パス

「E2Eでカバーすべきは、ユーザーが迷わず目的を達成できるか」だけだ。
それ以外はユニットテストに逃がせ。もし今、テスト自動化で苦しんでいるなら、一番落ちるテストを削除してみろ。意外と誰も困らない。むしろ、不要なテストを消すことで、本当に守るべきクリティカルパスにリソースを集中できる。

最後に:自動テストは「魔法」じゃない

自動テストは、書いた瞬間から「メンテナンス対象」という名の負債に変わる。

1. セレクタは `data-testid` 一択:UI構造に依存するな。
2. `waitForTimeout` は禁止:`expect` を使ったステートベースの待機を徹底しろ。
3. テストは独立させる:DBの共有は死を招く。
4. 絞り込め:E2Eは「ユーザー体験」の確認に特化しろ。

自動テストは魔法じゃない。泥臭いメンテナンスの積み重ねこそが、最強の品質保証になる。テストコードをプロダクトコードと同じ熱量でリファクタリングし続けろ。

書くことより、壊れないように守り抜く方が100倍難しい。だが、その先には「夜中にアラートで叩き起こされない」という、最高のエンジニアライフが待っている。