はじめに
自分が体験したテスト自動化による失敗をこの記事を通して共有します。これは同じような体験をしてほしくないという気持ちがあります。
また、あまり普及していないテストの考え方を体験談を通して伝えることが出来たらこれ以上ない嬉しさです。
結論
テスト自動化は毒にも薬にもなり得ます
エピソード
シードを使ったテストコード
テストコードのテストデータとしてシードを使ってしまったパターンになります。無意識のうちに意外とやってしまっているのではないでしょうか?
シード(Seeder)を使ったテストコードは保守しやすさが一気に落ちます。
データの構造が変わった時(特にリレーション関係が複雑なもの)や様々なパターンを網羅的にテストをしたい場合にシードがあると非常にコードが読みにくくなります。
また、必ずシーダーをメンテしなければいけない手間と「他の誰かがデータ構造をアップデートしたけれど、シーダーを更新しなかった」ときに地獄を見ます。
何のためにこのシードデータはあるのか?どのテストのためにこのシードデータはあるのか?
シーダーからは依存関係は読み取れません。該当するかもしれない場所をすべて探索する必要があるのです。
これがある程度小さな規模であればまだしも、中規模程度になるとテストの量も1000件以上に膨れてくるため一気に面倒で複雑でつまらない仕事が発生します。
もっと価値のあるもののために時間を使うため、極力メンテコストの高くなるシードデータを使用したテストを書くのは避けておきましょう。
一部例外として、参照データ(カテゴリのような)ものであればまだシードで持っていてもいいかもしれません。それでもオススメはしません・・・。
プライベートメソッドをテスト
Privateメソッドをテストするのは特別な理由がない限りはやめておきましょう。
特別な理由ですが、仕様化テストという現在あるものを壊さないためにするテストくらいだけでそれ以外はプライベートメソッドにテストを書くのはやめておいたほうがいいです。
ではなぜプライベートメソッドにテストを書いてはいけないのか?という点ですが、これは実装の詳細がテストコードに漏れ出してしまうからです。
非常に極端な例になりますが、usersテーブルから名前が1のユーザーを取り出すメソッドがあるとします。
この時、SELECT * from users where name = 1
というSQLを叩いているかどうかテストすることを実装の詳細がテストコードに漏れ出しているといいます。
このようなテストを書いてしまうと、ほんの少しでもリファクタリングをしただけでテストは壊れます。
もしそのようなテストが10件、100件あったらどうなるでしょうか?どんどんそのメソッドはリファクタリングはされずに放置され、悪魔のようなメソッドになっていくでしょう。(テストも含め)
自分はこの体験をしてしまい、テストのせいでリファクタリングできない状態にしてしまいました。これではソフトウェアの寿命は伸びません。
この経験から、なるべくオブジェクトの振る舞いをテストするように意識することを他の人には強く勧めています。
共通化の悪魔
まず最初に、良いテストコードとはDRYであることより、DAMPであることです。
DAMPというのはDescriptive And Meaningful Phrases(説明的かつ意味がわかりやすい言い回し)であるということです。
変にコード量をへらすために共通化が施されたテストコードは読みにくいですし、保守性もさほど高くはありません。
また、共有化した部分から微妙に変化が発生した場合、新たに切り出して修正する必要があります。
できるかぎりテストの本体部分を離れることなく、全体を理解することが可能なテストを作ることで、テストコードに対して向き合うコストを大きく減らし、メンテされなくなる悲しいテストコードを生み出さないようにするのです。
これを理解していなかった自分は無理な共通化をしてしまい、テストコードの内容を追うにも2個3個のファイルを参照する必要があり、その中で何をやっているのかも理解する必要がありました。
基本的には事前データを生成するものを別メソッドとして別ファイルに分けていたのですが、事前データを生成するだけでも複雑で、しかもテストコードの肝はテストデータにあるので必ず読む必要があり、テストコードをメンテナンスするのが非常にコストが掛かる作業になっていました。
しかし、データ構造を変えたりすると必ずテストコードを直す必要があり、テストコードを直さないと失敗するテスト(信頼できないテスト)がたくさん生まれてしまい、結局のところテストコードの意味がなくなってしまう・・・。
そしてテストコードが意味をなさなくなってしまうと、今までテストコードに費やした時間も無駄になるし品質の保証もなくなるので良いことは何一つありません。
繰り返しをへらすくらいの目的であるのであれば、説明的で意味がわかりやすいテストを書く方にコードを少し多く書いたほうが将来的に役立ちます。
テストコードが複雑
複雑というのはサイクロマティック複雑度が高いことを指します。
簡単に言ってしまうとif文が含まれていたり、ネストしていると複雑度は高まります。また、for文が含まれていても同様です。
テストコードが複雑ということは想像以上に問題を起こします。
テストコードを本番と同様に扱ってほしいのですが、どうしても本番コードよりは軽視されてしまいます。
それを踏まえた上で、ロジックが複雑に絡み合っているテストコードを誰がメンテナンスをシたいのか? という点で考えてみてください。
将来的にメンテナンスされずに、どんどん腐って厄介者になるテストコードの未来が見えます。
自分の経験でも、1週間前に書いたテストコードがif文で複雑に絡み合って居るのを見て「なんだこれ・・・」となり、結局その下に新しくメソッドを追加し、似たような内容のテストを付け加えてしまいました。
共通化の悪魔で書いた通り、テストコードの美徳はDAMPです。
メンテナンスされないテストコードになってしまうくらいならば、ちょっと多めにコードを書いてしまいましょう。
開発者しか読めないテスト名にする
結構やりがちですし、自分もたくさん書いてしまいました。
具体的に言うと「変数Xが○○以上だった時、成功する」といった形式です。
もっとひどいパターンだと意味のないテスト名(ControllerTest1, Test1, メソッド名 + Test)などもありますね。
テスト名でテストの内容を判別できないテストは価値が半減してしまいます。
ひと目で「どの動作が出来なくなっているのか」が確認出来ないのであればテストの意味がないですし、開発者以外もそれを識別できないとソフトウェアの品質の判断が付きません。
テストを書き、それを成功させる。他の場所でテストが壊れていたらそれを直して成功させる。というプラクティスだけが浸透してしまうといずれ「このテストはスキップ」「このテストはとりあえず成功にしておこう」という形式的なものになってしまいます。
また、コードを保守していくときも重要です。テスト名から「何をテストする」のかを理解できるため、テスト対象以外のものに気を配る必要がなくなります。
いちいち完全なデータを用意しないとテストとしてダメ!という環境よりも、「テストしたい対象に対して必要最低限のデータがあればいい」状態のほうが遥かにテストコードを書くコストは下がります。
出来る限りテストコードの意図を名前で示せるようになると保守しやすいコードになっていきます。
モックを多用してしまう
モックは便利です。モックを使うことで「コード」のテストがとてもしやすくなります。
しかし、テストをしたいのは「コード」ではなく、「ソフトウェア」です。
もし、モックで置き換えている依存先のクラスを変更し、そのモックを使っているテストコードが「変更前のクラス」の状態でテストしている状況になったら、それは本当に意味のあるテストになっているのでしょうか?
モックはモックです。本物のオブジェクトではないことを忘れないようにつかうことで真価が発揮されるでしょう。
以前モックをたくさん使われている状況に出くわした時があります。
そのソフトウェアはテストコードもたくさん用意されていて、テストもすべて通っている状態でした。
しかしバグがたくさん発生するのです。「テストが通っているのに何故?」と思いましたが、モックがたくさん使われている場所でのバグが多く、例外的な動作を見逃してしまっていたのです。
なるべくモックで置き換えてしまうのではなく、オブジェクトを使いましょう。実際のソフトウェアの品質を担保したいのであれば、実際動く通りに動かすほうが確実に信頼のできるテストになります。
おわりに
冒頭も書きましたが、テストコードは毒にも薬にもなり得ます。
そして、その手綱を握っているのは他でもないあなたなのです。
自分はたくさん失敗し、そこから同じことを繰り返されないように願い今回失敗談として共有しました。
誰かの参考になれば嬉しい限りです。