テストをリファクタリングするとき気をつけること

TDDのサイクルはRed-Green-Refactoringです。このリファクタリングは、プロダクトコードはもちろんですが、テストにも適用されます。しかしテストのリファクタリングには、プロダクトコードのリファクタリングと違うところもあります。

プロダクトコードのリファクタリングは、一言で言えば後で変更するときのためにやるものです。プロダクトコードを変更するのは、機能追加や変更、問題の修正のためです。つまり、将来プロダクトコードの価値を高めるためにリファクタリングをします。

テストのリファクタリングも、もちろん変更のためですが、テストコードの変更はプロダクトコードの変更に追随して発生します。テスト内容を変更するためにテストコードを直すことは稀です。したがって、プロダクトコードを変更しやすくするために、テストをリファクタリングする。

通常の(つまりプロダクトコードの)リファクタリングは、まさにリファクタリングしているそのコードをよりよくするためにおこないます。テストのリファクタリングを同じ感覚でやってしまうと、テストそのものは変更しやすくなっても、プロダクトコードを変更したときに対応する役には立たない、下手をするとかえって大変になってしまう、そういうことが起きます。

テストのリファクタリングをするとき、私が気をつけていることを紹介します。

テストは減らすのが一番

プロダクトコードを変更するために、テストコードの変更が発生する。ということは、テストコードは少ない方が、プロダクトコード変更のコストが減ります。ということは、単純にテストを減らせば変更に強くなります。不要なテストは消していく、これがテストのリファクタリングでは一番簡単で、効果も高いものです。

TDDをちゃんとやっていき、一通り完成すると、一連のテストが残ります。この中にはプロダクトコードの機能を担保するテストとして見たとき、価値の低いものがあります。

  • 異常系をカバーしていたが、今は決して発生しないケース
  • 外から調達したモジュールの機能を調べるときに書いたテスト
  • 意味的に同じテスト(同じ同値クラスや、プロジェクトコードのリファクタリングにより同じ内容になってしまったもの)
  • カバレッジなどのメトリクスを満たすためだけに書いたテスト(発生する例外をすべてカバーする、など)
  • TDDの細かいステップを刻むために書いたテストや、実質的に言語機能を確認しているようなテスト(newした結果がnullでないことを確認する、コレクションやイテレータの挙動を確認する、単純なSQL文をテストする、など)

こうしたテストは、問題が起きない範囲で消してしまいましょう。もちろん難しいのは「問題が起きない範囲」の見極めです。このテスト消しても大丈夫だろうか? なんでこんなテストがあるんだっけ? ずっと後でバグ作り込みの原因になったりしないだろうか? よく似たテストがあるけど、ちょっとだけ違うんだよね、どっちを残そうか? などなど。ある意味では、テストを書くより消す方がよっぽど時間と神経を使います。

消したいけれど自信がないなら安全寄りに倒して、消さないでおくようおすすめします。消しても大丈夫なものは、だいたいすぐに見つけられます。自信が持てないときは、往々にしてなにか隠れた理由があるものです。消すことは後でもできるし、消さずとも一時的に非実行状態で残しておくこともできます。自分では判断ができなくても、他に知ってる人がいるかもしれません。

安心して消せるものだけ消しても、テストは結構すっきりします。整理する中でテスト設計が立ち現れることもあります。まずは「消せるテストを探す」という視点でテストを見直すといいでしょう。

覗いていいのは一段階まで

テストは読みやすさが大事です。プロダクトコードが複雑なら、その中には要求の複雑さが反映されていて、プロダクトを把握する助けになります。テストコードが複雑なのは時間を無駄にするだけです。テストコードを読むときは、必要な情報にしたがって、以下の順で読んでいきます。

  1. テストの名前。これで分かればそれが一番
  2. アサート部分。名前でコンテキストや確認内容がわかれば、アサート部分を読めば正確な内容がつかめます
  3. フィクスチャ(コンテキスト)部分。ここまで読むのは状況がそうとうややこしいときです(デバッグ中とか)

これだけのことを把握するのに、せめてテストメソッドの中だけ読めばわかるようになっていてほしいです。何らかの処理をテスト間で共通化しているときも、共通化した関数・メソッドが十分わかりやすい名前になっていれば、中まで読まずに済みます。必要になったらメソッドの中身まで見ますが、共通化で頑張りすぎていると、1テストメソッドを把握するためにどこまでも深入りしてしまいます(図1)。


図1 共通化で頑張ると、読むのが大変

こうなると、テストを読むのにどれだけ時間がかかるか分かりません。前もって予測することもできないし、読んでいる最中もあとどれだけ残ってるのか見当がつきません。正直そんなに頑張りたくないという気持ちにもなりますし、だんだん何のためにどこを読もうとしていたのかわからなくなったりもします。

通化をする場合は、一段階まで、それ以上脳内スタックを使わないで済むようにしましょう(図2)。これならば、多少の誤差はあれ、読むべき分量の予測ができます。


図2 共通化が一段階だと、先が見通しやすい

テスト対象のオブジェクトが複雑だと、フィクスチャを準備するために大量の実装が必要になって、そのための共通化をしすぎてしまうことがあります。テストケースの数が多いときも、いろいろなバリエーションに対応するために複雑な共通化をやりがちです。パラメタ化テストでも、パラメタを準備する処理が多くなることがあります。テストを書くときには便利な省力化の工夫も、読むときには邪魔になってしまうのです。

アサート部分はまとめない

通化する話の続きですが、アサート部分がテストメソッド内で長くなったり、複数のテストケースで同じようなアサートを書くことがあります。これまた共通化したくなりますが、アサート部分をまとめて共通化するときは細心の注意を払いましょう。

アサートに失敗したときには、テスティングフレームワークによりますが、どこで何が起きたのか説明を表示してくれます。ここが共通化したメソッドの中だと、どのテストがどういう理由で失敗したかわからなくなってしまいます。テストが失敗した理由をデバッガで調べるなどというバカバカしい作業をしてはいけません。アサートで失敗したときの表示が具体的で有用になるようにしておきましょう。

複数のアサートをまとめて共通化すると、結果的になにをアサートしたいのか焦点がぼけやすくなります。共通化すると、それぞれのテストケースで確認したい内容をすべてまとめてアサートすることになります。その結果、本来テストケース内でアサートする必要がない箇所までアサートすることになってしまいます。これではテストがどこに注目しているのかわからなくなり、可読性を下げます。プロダクトコードの変更にも弱くなります。

アサートの部分を共通化するときは、フレームワークが提供するアサートと同レベルのアサートメソッドを独自に実装して、中途半端な共通化に終わらないようにします。HamcrestなどのMatcherを使うテストであれば、カスタムMatcherを実装すれば問題は起きにくくなります。

プロダクトコードに依存すると死ぬ

  1. 入力(たとえば文字列)を受け取って内部構造にパースする
  2. 中で内部構造の内容を更新する
  3. 内部構造を出力形式(たとえば文字列)にフォーマットして出力する

こういうパターンの機能があるとします(よくありますね)。単純に考えて、以下のようなテストを書くことになります。

i. 入力を渡して、内部構造が取り出せるか確認する
ii. 更新内容を確認する
iii. 内部構造を渡して、出力フォーマットが正しいか確認する

このとき、i.では内部構造の内容をアサートしたいのですが、そのまま内部構造をアクセスするのは面倒だったりします。文字列になってればすぐアサートできるのになあ...お、3.の処理を使えば内部構造を文字列化できるじゃないか!

同じことは、iii.のテストでも言えます。テストのために内部構造を準備するのに、1.の機能を使えば文字列から簡単に内部構造が準備できますね。

こうして、次のようなテストコードを書いてしまいます。

1の機能のテスト() {
  // i.入力を渡して、内部構造が取り出せるか確認する
  内部構造 = 1.の処理(テスト用データの文字列表現)
  結果 = 3.の処理(内部構造)
  assertEquals(結果, テスト用データの文字列表現)
}

3の機能のテスト() {
  // iii. 内部構造を渡して、出力フォーマットが正しいか確認する
  内部構造 = 1.の処理(テスト用データの文字列表現)
  出力 = 3.の処理(内部構造)
  assertEquals(出力 , テスト用データの文字列表現)
}

なんとテストメソッドが同じになってしまった! 「テストは減らすのが一番」の精神にのっとって、ひとつにまとめてしまおう!

...なんだか変ですよね。もともとは入力と出力それぞれのテストだったのに、「入力したものを出力すると同じになる」というテストにすり替わっています。1.と3.の処理に同じバグがあったら検出できませんし(両者を同じ人が書いていたら、きっと同じように間違えるでしょう)、仕様が変わって入出力が非対称になったら一気に破綻してしまいます。

テストの中で「テストのためにプロダクトコードを使いたい」欲求はよくあります。テストの中で、プロダクトコードとそっくり同じコードを書くハメになることも、ままあります。確かにテストを書く手間は省けるし、プロダクトコードの(きっとちゃんとテスト済の)機能を呼び出せば、安全だし読みやすくもあります。

しかしプロダクトコードにテストが依存してしまうと、プロダクトコードの変更がテストの不必要な変更につながります。場合によっては「プロダクトコードの通りに動いている」と確認するだけのテストになってしまい、品質が担保できなくなります。

テストの中でプロダクトコードを利用するのは、絶対にダメというわけではありませんが、避けた方がいいでしょう。作業量を節約したつもりが、あとで最悪のタイミングで帰ってくることになりかねません。


以上、テストをリファクタリングするときに気をつけるべき4つの点を紹介しました。