AIにPlaywrightのテストを書かせてPASSした。でも、そのテストは本当に確認したかったことを確認しているのか。
業務管理アプリを開発しながら、PlaywrightによるE2Eテストを作っています。
テストコードの作成にもAIをかなり使いました。
最初は、
「AIにテストコードを書いてもらえば、回帰テストをかなり効率化できるのではないか」
と考えていました。
実際、効率化できた部分はあります。
でも、しばらく続けてみて分かったのは、
AIが書いたテストコードも、テスト対象と同じくらい疑う必要がある
ということでした。
今回は、業務管理アプリのE2EテストをAIと一緒に作りながら実際に困ったことと、そこから変えたテストの進め方をまとめます。
テスト対象は業務管理アプリ
今回テストしているのは、自分で開発している業務管理アプリです。
取引先、案件・契約業務、作業依頼、作業記録、カレンダー、業務報告、請求、見積などがつながっています。
たとえば、
取引先 → 案件・契約業務 → 作業 → 業務報告 → 請求
というように、一つのデータが後続の機能でも使われます。
そのため、単純に「登録ボタンを押して成功した」で終わるテストにはできません。
登録したデータを別画面から使う。
使用中のデータは削除できない。
権限によって表示されるデータが違う。
テスト終了後には作ったデータを削除する。
こうしたところまで確認する必要があります。
ブラウザは主にEdge、実行時は workers=1 を基本にしました。
今回のテストは、同じ開発環境のDBに対して取引先や案件などのデータを実際に登録・削除します。また、アプリ側にはログイン状態に関する制約もあります。
そのため今回は、並列実行によるテストデータやログイン状態の干渉を避け、まずテストそのものを安定させることを優先して workers=1 にしています。
npx playwright test tests/locolisu --project=edge --workers=1
これは「Playwrightは並列実行しない方がいい」という意味ではなく、今回のアプリとテスト環境に合わせた運用です。
AIにテストを書かせれば終わり、ではなかった
AIに、
「この画面のこの動作をテストしたい」
と伝えれば、Playwrightのコード自体はかなり書いてくれます。
問題は、そのコードが本当に確認したかったことを確認しているかです。
テストが失敗すると、最初はアプリ側に不具合があるように見えます。
でも調べていくと、
アプリではなくテストコードが間違っていた
ということが何度もありました。
逆もあります。
テストがPASSしている。
でもコードを確認すると、
本当に確認したかった条件を確認していない。
これではPASSしていても意味がありません。
ここから、私の中でテスト結果の見方が変わりました。
「PASSした」は結果であって、正しさの証明ではない
Playwrightが、
106 passed
と表示すれば、気分はいいです。
でも重要なのは106という数字ではありません。
その106件が、
何を、どの条件で、どのように確認したのか。
こちらです。
たとえば、
「保存後に対象データが存在すること」
を確認したいとします。
対象の特定方法が曖昧なら、別のデータを見つけてPASSしている可能性があります。
表示文字列だけで対象を探せば、同じ文字列を含む別の要素を拾うかもしれません。
画面上に要素が存在することだけ確認しても、それが今回登録したデータなのかは別問題です。
つまり、
assertionがあることと、正しいものをassertしていることは違います。
AIがコードを書いた場合、この違いを人間側で確認する必要があります。
一番厄介だったのは、テストデータの後片付け
E2Eテストでは、実際にデータを登録します。
取引先を作る。
案件を作る。
作業明細を作る。
そしてテストが終われば削除する。
正常終了するだけなら、それほど難しくありません。
問題は、
途中でテストが失敗した場合です。
たとえば、
- 取引先を登録
- 案件を登録
- 作業明細を登録
- assertionで失敗
となった場合、何もしなければ1〜3で作ったデータが残ります。
次のテストでは、その残ったデータが別の失敗原因になります。
最初はアプリの不具合を調べていたはずなのに、途中から、
前回のテストが残したデータによって今回のテストが失敗する
という状態になります。
これがかなり厄介でした。
cleanupには削除順序がある
さらに、作ったものを適当に削除すればいいわけでもありません。
今回のアプリでは、データ同士に依存関係があります。
案件が作業明細で使われていれば、その案件を削除できません。
案件が残っていれば、その案件が属する取引先も削除できません。
そのため、後片付けは概ね、
作業明細 → 作業依頼 → 案件・契約業務 → 取引先
というように、参照している側から戻って削除する必要があります。
ここを間違えると、
「cleanupに失敗した」
という新しい問題が発生します。
しかも、テスト本体が失敗したあとにcleanupまで失敗すると、原因が一気に分かりにくくなります。
E2Eテストでは、
テストデータをどう作るかと同じくらい、どう消すかを最初から考えておく必要がある
と実感しました。
「成功したらcleanup対象にする」では遅い場合がある
もう一つ問題になったのが、どの時点で「このデータは削除対象」と記録するかです。
たとえば取引先を登録するテストなら、
「登録処理が全部終わった」
あとでcleanup対象に追加したくなります。
でも、登録自体はDBに成功していて、その直後の画面確認でテストが失敗することがあります。
この場合、
データは存在するのに、cleanup側は存在を知らない
という状態になります。
そこで、テストデータについては、
作成されたことを確認できた時点で、できるだけ早くcleanup対象として扱う
必要がありました。
これは小さな違いですが、残留データを減らすうえでかなり重要でした。
テストが失敗したとき、すぐアプリを直さない
AIを使ったテストで特に危険だと思ったのがこれです。
テストが失敗する。
AIにエラーを渡す。
AIが、
「原因はアプリ側です。ここを修正します」
と判断する。
そのままアプリを修正する。
でも、実際にはテストコードの方が間違っていた。
これをやると、
正しく動いていたアプリを、間違ったテストに合わせて壊す
可能性があります。
実際の運用では、テスト失敗時に少なくとも、
仕様 → 実画面の動作 → アプリ側コード → テストコード
を照合してから、どちらを直すか判断するようにしました。
「テストが赤いからアプリが悪い」
とは考えない。
これはAIを使う場合、かなり重要だと思っています。
AIは以前の成功パターンにも引っ張られる
AIとのテスト作成では、もう一つ注意が必要でした。
ある方法で一度テストが成功すると、似た画面でも同じ方法を使おうとすることがあります。
でも、DOM構造やデータの持ち方が違えば、同じ方法が正しいとは限りません。
以前成功したlocator。
以前成功したデータ取得方法。
以前成功したcleanup。
それを別のテストへそのまま持ってくると、うまくいかないことがあります。
そのため、
「前にこれで動いたから」ではなく、今回の画面とコードを確認して判断する
ことをAI側にも求めるようになりました。
人間でも同じですが、AIはコードを大量に作れる分、間違ったパターンも大量に展開できます。
テストを増やすことより、壊さないことを優先した
途中から、テスト追加の進め方も変えました。
新しいテストを作るたびに既存テストまで大きく書き換えると、何を確認していたのか分からなくなります。
そこで、
- 既存テストを勝手に削除しない
- テスト件数を理由なく減らさない
- 成功済みテストを安易に書き換えない
- 修正対象をできるだけ限定する
- テスト失敗時にアプリ側を先に直さない
- cleanupまでテストの一部として考える
- 全体テストを毎回最初から実行しない
という運用に寄せていきました。
特に最後は重要でした。
一か所直すたびに全E2Eテストを実行すると、テスト自体に時間がかかります。
まず対象テストだけを実行する。
通ったら関連範囲を確認する。
最後に全体を回す。
この順番にしないと、修正よりテスト待ち時間の方が長くなります。
AIに任せる範囲も変わってきた
今回の経験で、
AIにコードを書かせること自体が問題なのではない
と思っています。
むしろPlaywrightのコード生成にはかなり使えます。
ただし、人間側が、
「何を確認したいのか」
を固定しておく必要があります。
AIに任せやすいのは、コードの作成、既存コードの調査、locator候補の整理、重複処理の整理など。
一方で、人間側に残した方がいいのは、
そのテストが何を証明するのかという判断です。
ここまでAIに任せると、
「テストコードはきれいになった。でも、本来確認したかったものが消えた」
ということが起こり得ます。
今のところ決めていること
今回の業務管理アプリでは、AIを使ったE2Eテストについて、少なくとも次のように考えるようになりました。
- PASS件数だけを見ない
- 失敗したら、まずアプリとテストのどちらが間違っているか確認する
- テストデータは作成時点からcleanupを考える
- 依存関係の逆順で削除する
- 途中失敗してもデータを残さない
- 既存の成功済みテストを安易に壊さない
- 対象テスト → 関連テスト → 全体テストの順で実行する
- AIが作ったassertionが、本当に目的を確認しているか人間が読む
まだこれが最終形だとは思っていません。
今回のE2Eテスト自体も、何度も作り方を変えています。
ただ、
「AIにテストを書いてもらえば、テストまで自動化できる」
というほど単純ではないことは、かなり分かってきました。
AIが書いたテストだからこそ、テストを読む
AIによって、テストコードを書く作業そのものはかなり任せられるようになりました。
でも、コードを自分で一行ずつ書かなくなるほど、
何を確認するためのコードなのかを人間が見失わないこと
が重要になると思っています。
アプリのコードをAIが書く。
そのアプリを確認するテストコードもAIが書く。
そして、
「テストが通りました」
とAIが報告する。
ここまで全部AIに任せると、見た目としては非常にきれいです。
でも、その間に人間が一度も、
「このテスト、本当に確認したいことを確認している?」
と見なければ、かなり危ない。
今回の業務管理アプリでは、そこに何度も引っかかりました。
だから今は、
AIにテストを書かせるなら、むしろテストを読む。
それくらいでちょうどいいと思っています。