こちらの講演時にいただいた質問への回答です。

「テスト自動化とテスト駆動開発」講演資料 - やっとむでぽん

質問6. テストコード部分の品質はどのように維持するのでしょうか?テストコードのテストコードを作るのではないと思いますが……

ひとつは、プロダクトコードと同様のレビューとリファクタリング。もうひとつは、テスト駆動開発に特有ですが、プロダクトコードとテストコードがお互いに支え合うような関係にする考え方です。

テストのレビュー

まずはレビューですが、これは普通のやり方です。テストコードの書き方、テストの内容、他のテストと見比べた上で全体を把握しやすく書けているかなどを見ます。特に、テストコードや実行結果から、テスト一覧を出力したり、失敗時の表示などを見て、どんなテストをしているか把握できると、テストドキュメントをある程度代替できるので、そうした観点で読めるか・伝わるかも見ます。

プロダクトコードのレビュー時に、TDDで書いたテストコードも一緒にレビューするのも一般的です。急いでいるときはテストだけレビューすれば、何が動いているか、どこまで進んでいるかわかるというメリットもあります。

極端に品質の低いテストコードには、以下のようなものもあります。

  • テストの実体がない(ガワだけ。書き忘れ、時間不足)
  • テストが実行されていない(タイプミスなどで、けっこうやりがち)
  • なにも検証していない(assertがない。テストを理解できていない。コードカバレッジを増やすためにやってる場合もあるが、無意味)
  • テスト対象のプロダクトコードが、なぜかテストコードにコピーしてある(テストへの理解不足。「このコードをテストしたいんだ」という気持ちはわからなくもない。コピペ開発のやりすぎかも)
  • 書いた人の環境、PCでないと実行できないテストになっている(ファイルアクセスが必要、特殊なテストデータが必要などで、全員が実行できるようなテスト環境を整えられていない場合に見られる。開発ができる人ほど、できちゃうので、やりがち)

メンバーの理解不足、スキル不足が原因の場合もありますが、時間がなくてつい…ということもあります。人の目によるレビューは、だいたいいつも有用です。ペアプログラミングをしていれば、レビューは減らしても大丈夫かもしれません。ペアプロでも、コードレビューをまったくゼロにするチームは少ないようです。

テストコードもリファクタリング

レビューからのフィードバックを取り込むのは、テストコードのリファクタリングです。もっとも、レビューがなくても、リファクタリングではいつでもテストコードを対象にします。ここでは「コードとしてのリファクタリング」と「テストとしてのリファクタリング」を両方とも意識する必要があります。

テストコードもコードなので、プロダクトコードと同様、読みやすく、重複なく、変更しやすいようにリファクタリングをします。基本的にはプロダクトコードのリファクタリングと同様です。ただしテストコードの場合は、「どんなテストケースなのか」を読み取れるのが最優先となります。プロダクトコードは、コードを書く人しか通常読みませんが、テストコードを読む人はテストの内容を知るために読む場合が多くなります。そうするとロジック(How)を把握するより、何をしているのか(What)を把握するのが早いほうが、嬉しい。

私は、読む人は以下の順にテストコードを把握したいだろうと想定して、できるだけ早く必要な情報がわかるように意識しています。

  1. どんなテストか(What) - テスト名(メソッド名など)を見ればわかるように
  2. 確認している内容は何か - assert部分を1行だけ見ればわかるように
  3. どうやってテストしているか(How) - テストメソッド全体(だけ)、特にassertまでの準備部分を見ればわかるように
  4. テストを書き足すにはどうするのか - 共通化されたセットアップ、フィクスチャやヘルパーを含めたテストコード全体を見る

また、少しずつ異なる複数テスト内容が、少しずつ異なる複数のテストメソッドで実装されます。この「少しずつ」の差異は、テストコードの中に埋もれてしまって読み取りづらくなりがちです。「あれ、このテストとこっちのテスト何が違うんだろう…(5分後)あー、ここが123546789か123465789かの違いかー」みたいになります。この間違い探しはツラいし時間を食いますし、さらに違いを見落とす場合もあります(実はさらに"あああああああああ"と"ああああああああああ"の違いがあった、ということもある。なお、こんなテストを書いた人は呪われてしまえと思う)。

こういう差分が素早く確実に読み取れるような工夫も、テストコードには大事です(特有なわけではなく、プロダクトコードでも役に立ちますけれど)。定数を使う、インデントを揃える、コメントを入れる、名前付きパラメータやデフォルト値を活用する、フィクスチャなどで再利用する、前準備をテストのネストで構造化する、ヘルパー関数を作る、カスタムアサーションを作るなどのテクニックがあります。

テストとしてのリファクタリングも、ここで考えるべきです。テスト駆動開発で書くテストは、開発を進める順番に書いていくので、後から見ると「なにをテストしてるのか」「なんでテストしてるのか」わからなくなりがちです。ひとまとまりの機能が完成して、その機能をテストがだいたいカバーしているタイミングで、今までに書いたテストを見渡します。一貫性があるか? 仕様、なにをする機能か(What)がテストから読み取れるか? テストどうしの見分けがつくか? 全体としてどんなテストをしているか把握できるか? 他の機能のテストと並べてみて、意味があるか? テストの重複はないか? 足しておきたいテストは?

作業が一区切りついたとすると、次にこの部分をさわるのは、もしかしたら半年後かもしれません。この部分をまったく知らない別の人(ペア)が、また新しい仕様変更に対応するかもしれません。そのとき、テストが作りっぱなし、書きっぱなし、半年前に作業したペアにしかわからない状態だと、ひどく苦労することになります(そして元々のペアも半年前のことはもちろん覚えていません)。「テストが整理されていない」状態では、テストを適切に変えることもできなければ、テストを命綱として安全に変更することもできなくなります。

テストコードを他の人が読むことを想定し、またテストの内容や網羅性、観点やテスト全体での位置づけを伝えるのが大事だという認識で、テストコードをリファクタリングします。テスト名を仕様の言葉で表現し、正常系なのか異常系なのか、境界値をテストしてるのか、組み合わせをテストしているのか、見分けがつくようにします。テストをネストして分類したり、同種のテストをグルーピングしたりします。パラメタ化テストも便利です。こうしてリファクタリングされたテストは、QAテストでも役に立ちます。

テストのリファクタリングでもっとも難易度が高いのは、テストの削除です。すでにあるテストが、重複しているように見えるのだけど本当に消していいのかわからない…ここで消してしまう勇気はなかなか出ません。実際問題として、自信を持ってテストを消せるのは、テストを書いた当人だけ、テスト書いた直後だけです。書いたばかりのテストをリファクタリングするタイミングであれば、これはこういう事情で書いたけど、もういらないという判断ができます。不要なコードを後に残すのは、メンテナス性を下げるだけです。不要なテストの削除も含めて、テストのリファクタリングを忘れないでください。

コードとテストが支え合う

もうひとつ、テスト駆動開発ではプロダクトコードとテストコードを使ってお互いに動作確認をしあうことがあります。テスト駆動開発の技法として仮実装(Fake It)というものがあります。これは、テストコードを書いた段階で、確実にテストを成功させる(Greenにする)ため、プロダクトコードに偽物(Fake)の簡易な実装をします。実際の計算をする代わりに固定の値を返すなどです。もしもこの時点でテストが成功しなかったら、問題があるのは(固定値を返すだけの)プロダクトコードではなく、テストコードです。こうして、テストコードが正しく動作するかを確認します。テストのための初期化が足りていなかったり、呼び出す対象が間違っていたり、結果の確認方法が違うなどの問題を検出できます。

同じアプローチは、書き始め以外でも使います。コードのこの部分を変えると、このテストがこういうふうに失敗するはずだと考えて、実際に変えてテストを実行します。ここでテストが失敗しなかったり、失敗しても想定と違うメッセージを出していたりすると、テストが間違っている(あるいは自分の理解が違う)とわかります。

TDDのレッド・グリーン・リファクタリングのサイクルは、数分くらいとごく短いですが、このアプローチを使っているときは

  1. 試しに1文字変えて実行→
  2. 期待通りRed→
  3. 正しいつもりで数文字直して実行→
  4. 期待通りRed→
  5. さらに数文字直して実行→
  6. 期待と違うRed→
  7. いったんコードを戻してGreen→
  8. テストを直してRed→
  9. 正しく直してGreen

というように、1分以内、秒単位のサイクルで、コードとテストの挙動を細かく確認しながら進める場合もあります。こうして、テストが機能していなかったり、想定と違うテストをしてしまっているような状況を防ぎます。