http://martinfowler.com/bliki/ErraticTestFailure.html

先日、本のサンプルコードを書いていたときのことだ。 いくつか変更を加え、動くのを確認し、テストを走らせ、レポジトリにコミットした。 今度は別のコードをいじりはじめ、いくつか変更を加えた……ら、前の場所で予期せずテストが失敗した。 自動テストで次にやるべきことは、この予期せぬ失敗の該当個所を見つけることだ。 だが、この本のサンプルコードは(変更を行った箇所とは)完全に独立している。 おかしい。

デバッグするまえに、diffデバッグを試した。 コミットしてから何も変更していないから、svn revertを行った。 そして、テストを走らせた……ら、また失敗した。 コミット前にテストはパスしたはずだ。 今度は、IntelliJの代わりにantでテストを走らせてみた。テストはすべてパスした。 同じディレクトリにあるJUnitのクラスを実行している同じテストなのに、 なぜantではパスしてIntelliJでは失敗するのだろうか?

そのとき、不覚にもこう考えてしまった。 「IntelliJが悪いんじゃねーの。キャッシュしてたりしてrevertしたのが反映されてないとか」。 若かれし頃、先輩プログラマに「デバッグのルール其の壱」を教わった。 「バグは常にコードの中にある。コンパイラの中にあるのではない」。 だが愚かにも私は、IntelliJを再起動した。そしたら、あら、テストがパスした。 これにて一件落着めでたしめでたし……って、否ッ! この奇妙な振る舞いは再び起こることになった。 だがそのとき、運良くSergeyとペアプロをしていた。 彼は私のように愚かな行動をせず、さっさとバグを見つけてくれた。

この種の問題の答えを見つけるには、屋外へ出て、高さ6.5フィート(約2m)の文字で言葉を作るんだ。 杉で文字を作れば、色は塗らなくても大丈夫——だが、さくらんぼの飾りつけをわすれちゃだめだ。 その言葉とは、

分離(isolation)。

コードを変更していないのに、テストがたまにパスして、たまに失敗するような場合。 また、あるsuitesの中ではパスするのに、他のsuitesの中だと失敗するような場合。 そんなときは、十中十一、テストの共通データが正しく初期化されていないのが原因だ。 そのような状態でテストを走らせたりするから、テストはパスしたり失敗したりする。 結果として、テストが断続的に失敗するようになる。 再現できないから、タチが悪い。

私はJUnitを使っていたが、JUnitは分離を重要視している(だからJunitNewInstanceという振る舞いしているのだ)。 私の問題は、staticなデータが原因だったに違いない。 この場合、現在日をとってくる呼び出しだった。 ClockWrapperを使っていたが、あるテストの中でそのイニシャライズに失敗していた。ちゃんとイニシャライズするテストが先に走るかどうかで、いくつかのテストが成功したり、失敗したりする。

ここから2つの教訓を得ることができる。 まず、可能な限りテストデータを分離すること。 毎回、新しいデータを作成すること(テストの実行スピードを犠牲にしても)。 テストをうまく分離させれば、こういった問題には出くわさないようになる。

次に、こういった問題に出くわしたときは、 テスト間でデータを共有していないか調べること。 テストがデータを完全に初期化しているか調べること。 初期化されていなければ、いつ作られたのか、変更が行われたかどうかを確認すること。