仕様変更によって修正が必要になるテストケースの数
ここで1つ考察しておきましょう。仕様変更のとき、思いがけない数のテストコードを修正するはめになって、泣きそうになることがあります。仕様変更のときに修正するテストコードが膨大にならないようにするには、どうしたらよいのでしょう。
テストコードもリファクタリングを欠かさない
テストを実行するための準備や後始末に複雑な手順が必要な場合、その部分、例えばオブジェクトの生成手順に仕様変更が入ると、テストコードにも面倒な修正をしなければなりません。テストメソッドをコピー&ペーストで作っていると、その面倒な修正をテストメソッドの数だけ行うはめになります。
準備や後始末の手順が複雑な場合(後で修正することになったら面倒だなと思えるとき)は、テストコードをリファクタリングしてその手順をできるだけ一か所にまとめておきましょう。NUnitでは、今回のサンプルコードのようにTestCase
属性を使って書くと楽です。あるいは、準備するコードを別のメソッドに切り出したり、テストクラス全体で同じ準備が必要ならSetup
にまとめるなど、工夫してみてください。
テストケースは最小限にする
テストファーストに必要なテストケースよりも、たくさんのテストケースを作っていませんか? 製品コードとは無関係に作る品質保証のための単体テストと、テストファーストは違います。
テストファーストで作るテストケースの数は、すべてを明白な実装で行った場合、対象のメソッドのサイクロマチック数と同じになります。仮実装 ⇒ 三角測量を行うと、そのぶんだけ+1されます。さらに、安心を得るために、GREENとなるはずのテストを追加することもあります。それでも品質保証のための単体テストよりは、テストケース数は少ないはずです。
サイクロマチック数とテストケース数
サイクロマチック数(循環的複雑度、Cyclomatic Complexity)とは、線形的に独立した経路の数です。例えば次の図のメソッドX()
には、処理B1
を通る経路と処理B2
を通る経路があるので、サイクロマチック数は2です。
このメソッドX()
をテストファーストで作っていくことを考えてみます。まず、処理A
⇒ 処理B1
を通る経路を実装するために、明白な実装でやるなら1つのテストケース、慎重に仮実装と三角測量をやるなら2つのテストケースを書くことになります。つぎに、処理B2
を通る経路の実装のために、明白な実装でテストケースを1つ追加します。これでもうREDになるテストケースは書けなくなったはずですから、テストファーストは終了です。書いたテストケースは2個または3個(最初に三角測量を行った場合)になります。一方、品質保証のための単体テストでは、同値クラス2つと、その境界値前後の2つ、合わせて最低でも4個のテストケースが必要になります。
特に不安があるわけでもないのに、品質保証のための単体テストと同様にテストケースを書いてしまってはいませんか? 必要のないテストケースを増やしてしまうと、仕様変更が入った時に修正なければならないテストケースも増えてしまいます。
TDD三原則
不必要なテストケースを書くことなく、しかもC0カバレッジは100%を達成するためのルールがあります。Robert C Martin氏が提唱した「TDD三原則」(原題は”The Three Laws of TDD”)です。
(翻訳:安井力氏)
- 失敗するユニットテストを成功させるためにしか、プロダクトコードを書いてはならない。
- 失敗させるためにしか、ユニットテストを書いてはならない。コンパイルエラーは失敗に数える。
- ユニットテストを1つだけ成功させる以上に、プロダクトコードを書いてはならない。
「TDD三原則」といながら、内容はテストファーストに関することだけなので注意してください。また、この原則から外れて、不安を解消するためにGREENになるはずのテストを書いてもよいのです。このルールに従ってテストファーストを進めると、最小限のテストケース数で済むということです。
シンプルなメソッドにする
さきほどのメソッドX()
のテストケース数が最小の2個だったとします。処理B1
の部分に仕様変更が入った場合、修正すべきテストケースは、1つのはずですね。では、処理A
に仕様変更が入ったらどうなるでしょう。メソッドX()
のテストケースすべて(ここでは2個)を修正する必要があります。サイクロマチック数が大きいメソッド(すなわちテストケースが多い)で、共通に実行される部分に仕様変更があると、たくさんのテストケースを修正しなければならなくなります。
テストファーストから見たシンプルなメソッドとは、
- メソッドごとのサイクロマチック数を小さくする
- 共通に実行される部分を通るテストケースを少なくする
1つ目は、メソッドを分割することで個々のメソッドをシンプルにするということです。そうすることで、個々のメソッドは外部設計が考えやすく(=テストが書きやすく)、しかも仕様変更時の修正箇所は少なくなります(なお、publicにしたくないメソッドは、InternalsVisibleTo属性を活用してinternalにします)。
また、ロジックを書き直す(今回のサンプルコードでは、switch
文をやめてif
文で書き直す)ことで、サイクロマチック数を小さくできることもあります(この場合、不要になったテストケースは削除する)。
2つ目は、共通に実行される部分に仕様変更があると大変なので、そこを通るテストケースは減らすようにしましょうということです。例えば、メソッドX()
から、次の図のようにメソッドY()
を分離してみましょう。
トータルのサイクロマチック数は変わっていませんし、トータルのテストケース数は増えています。しかし、処理A
を通るテストケースは1つ(三角測量するなら2つ)になります。処理A
に仕様変更が入った時に修正しなければならないテストケースを、減らすことができました。
ただし、新しいメソッドX()
のテストケースは、処理B1
か処理B2
のどちらかに依存しています。例えば処理B1
に依存していて、そこに仕様変更があると、こんどは処理B1
とメソッドX()
の両方のテストケースを修正しなければならなくなります。この2つ目の指針は、サイクロマチック数が大きいときに有効なのです。
例えばメソッドY()
のサイクロマチック数が10だったとすると、その経路の共通部分に仕様変更があるたびに10個(あるいはそれ以上)のテストケースを修正するか、経路のどこに仕様変更があろうと修正は1個または2個で済むようにするかという選択になりますから、その場合は分離しておくのが正解でしょう。
別解として、新しいメソッドX()
のテストケースで使う期待値を、メソッドY()
の戻り値を使ってテストケース内で算出するという方法もあります。この方法には賛否あると思うので、ここでは詳しく述べません。