続・テストコードのリファクタリング

テストコードのリファクタリング - 千里霧中」の続きです。

十分に実施できる方法

 テストコードを対象としたリファクタリング回帰テストについてですが、現実性があり、十分に実施できる方法は主に次の2つとなると思います。

  1. テストコードのインプットとなるテストケース仕様にもとづいて、ミューテーション分析を実施。ミューテーションテストと正常系のテストを実施することで、バグがなければパスし、バグがあれば失敗することを確認する。
  2. テストコードに対する入出力・間接入力(テストフィクスチャからの入力など)・間接出力(Assertionメソッドへの出力等)を、Test Doubleやロギングで網羅的に記録。変更前と変更後で、入出力、間接入力・間接出力が変化しないことを確認する。

 ただ現実性があるといっても問題もあります。ミューテーション分析については、テストケース仕様からミューテーションテストの仕様を作成する作業にミスがあると、十分性が保てなくなります。テストケース仕様がテストコードと対応しているか判断する基準をテスト目的に応じて決めなければなりません。またテストケース仕様がない場合は、テスト設計をリバースするという困難な作業を行う必要があります。これらの作業の正確性を完璧にしようとすると、モデル駆動や証明の世界に入らなければならなくなります。
 また入出力、間接入力・間接出力が変化しないことを確認する点については、そもそも難しい場合があるのと、手間もかかります(例えばMockオブジェクトを使われると作業難易度が高まります)。

現実的な方法

 一方で、それ以外では、 前回でも軽く触れましたが、やはり軽快かつ信頼できる決定的な手段はありません。特に高速にテストコードのテストをしなければならない場面では、不十分なりに「コードレビューによるチェック」「グリーン状態を維持していることをチェック」といった手段を組み合わせて安全性を高めていくことになると思います。例えばTDDのように軽快なテストコードの変更が要求される場合では、回帰テストが不十分な場面をある程度許容しなければならないでしょう。
 そのため「テストコードのリファクタリング」という言葉には、回帰テストがきちんとできているか怪しい点で、若干の後ろめたさが含まれています。

 現実解としては、テストの要求や制約(どれ位テストコードの透過性を保障性なければならないか、テストはどれぐらい軽快に実施しなければならないか)を考慮して、前エントリで上げた手段等を(組み合わせを含め)取捨選択していくのが無難なアプローチになると思います。

不十分さを支える

 なお不十分さを許容しなければならないならば、それを補う対策を常日頃から実践しなければなりません。特に以下に挙げているような、変更性や解析性を向上させるテストコードの実装の工夫は重要です。

Test Methodをコンパクトかつ明快に記述する

 例えばTest Methodは十分に細分化し、Test Method内にあるAssertion Methodの数を絞ります。またAssertion Methodはループや複雑な分岐の中で記述するのを避けます。

影響範囲を限定する

 例えばAssertion Methodのステートメントからは副作用を排除し、Assertion Methodの順番を入れ替えたり、移動させたりしてもテスト結果が変化しないようにします。
 またTest MethodsやTest Suiteはそれぞれ独立させ、他のTest MethodsやTest Suiteに影響を与えないようにします。具体的には、あるTest Methodsの実行有無やテスト結果の変化で、別のTest Methodsのテスト結果が変化してしまうような構造は避けます。また、あるTest Methodを変更したら、他のTest Methodの結果が変わってしまうような構造も極力排除していきます。
 これらは変数や関数のスコープを限定する、Test Doubleの活用で副作用を持つコンポーネントを避ける、Setup・ロールバックを充実させるなどといった手段で実現されます。

時間軸から独立させる

 実行するタイミングや処理速度でTest MethodやTest Suiteが変化しないようにします。
 これらは実時間に依存するコード、タイミングに依存するコードの排除で実現されます。なおもし実時間やタイミングを扱わなければならないテストがあるならば、他のテストプロジェクトに分離するか、あるいは別種のテストとして運用しましょう。

重複を排除する

 例えば、重複部分(コピペ部分)はメソッド抜き出し、定数化、Setup Method、Teardown Methodへの委譲で共通化します。
 またParameterized TestやDDTの構文の活用でデータとロジックを分離し、ロジックを共通化してしまうといった対策も有効です。

適切な名前を与える

 テストに用いる変数、Method、Classにはテストの意図を表現する適切な名前を与えます。
 またテストで扱うデータ(値や文字列)も、適切な名前を付けた定数で置き換えたり、値そのもので意図を表現したりして、意図を明示しましょう。例えば「無効な名前」という意図をもった文字列なら、「"Yamada Taro"」のような意味不明な文字列でなく、「"Invalid Name"」という意図を伝える文字列をストレートに用いたり、あるいは「InvalidName = " Yamada Taro";」のように分かりやすい変数に置き換えてしまいましょう。


 上記のような工夫を実践すると、スコープや影響範囲がコンパクトに限定されかつ可読性に優れたテストコードを構築できます。そうなればリファクタリングや変更時のミスも大きく削減できるでしょう。
 なおユニットテストであってもその実装はプログラミングそのものなので、他にも多種多様なプログラミングの工夫が導入可能です。製品コードとテストコードは区別せず、どんどんデザインパターンや実装の工夫を適用していくべきです。

まとめ

 ユニットテストの対象としてみると、テストコードはいわばテスト困難なコンポーネントに依存する厄介なコードと見なせます。極端に言ってしまえばGUI等のコードと違いありません。そのため一連のエントリで解説したように、テストコードに対する十分なテストの確保には色々と困難が伴います。
 それゆえ、テストコードの継続利用や再利用を行う場合は、リファクタリングや変更を容易にするための工夫をテストコードにも実施しなければなりません。