質の高いテストコードが備える4つの特徴とそのバランスの取り方

はじめに

今回はテストコードが備えるべき4つの特徴と

それらのバランスの取り方について、

本(単体テストの考え方/使い方)を読んだり、

現場でテストコードを書いたり、テストコードや設計の改善を行ってきた自分の体験

を基にまとめてみました。




この4つの特徴のうち3つは相反し、

さらに4つの特徴のうち1つでもゼロ(全く特徴を備えない)

があるとテストコードの価値がなくなります。

テスト対象に応じて、

また、テストコードで検証すべきことに応じて

どの特徴を最大限備えさせつつ、

他の特徴に対してもバランスをとることが、

価値の高いテストコードを作るのに重要になります。


質の高いテストコードの特徴その1:退行(regression)に対する保護

プロダクションコードを変更した際、

元々動いていた既存機能が動かなくなるような退行に対して、

保護(検知)する仕組みは重要です。

その退行に対する検知の精度が高いほど質の高いテストコードといえます。




退行に対する保護がテストコードにどれくらい備わっているかを把握するには、

  • テストコード実行時に多くのプロダクション・コードを実行できているか??
  • テストコードに実行されるプロダクション・コードは、ビジネス的に重要で複雑なコードか??

これらの観点でテストコードとテスト対象を分析する必要があります。




質の高いテストコードの特徴その2:リファクタリングへの耐性

プロダクションコードにはリファクタリングを頻繁に行うことで、

無秩序なコードを整理して開発スピードを保てるようにすることが重要です。




プロダクションコードの振る舞いを変えることなく、

実装の詳細を変更するこのリファクタリングを行った場合、

テストが失敗してはなりません。




もし、テストが失敗するようになるということは、

テストコードがテスト対象の機能が正しく振る舞っていることを

正しく検証できていなかったということになります。

(偽陽性:false positive)




リファクタリングへの耐性が備わっていなければ、

それは質の高いテストコードとはいえません。




実際に自分がいた現場での例ですが、

テストコードをかなり丁寧に書いているのに、

テスト対象を外から見た振る舞いだけでなく、

実装の詳細を細かく検証しているせいで、




少し実装の詳細を変更するリファクタリングをかけただけで、

テスト結果がほとんどNGになる。

(外から見た振る舞いは変更していないので本当はテスト結果OKになるべき)




このせいで、

リファクタリングとセットでテストコードを修正する必要があり、

ちょっとしたリファクタリングに必要以上に工数がかかる。

といった体験をしたことがあります。




これがひどくなると、、、

次第に開発者はテスト結果が信頼できずテストコードを無視するようになり、

結果として問題のあるコードが本番に持ち込まれるようにもなります。




テストコードにリファクタリングの耐性がなければ、

安心してリファクタリングを行うため、

工数をかけて作成していたはずのテストコードが、

いつの間にか、リファクタリングの足枷になってきて、

これならテストコードがない時と状況が大して変わらない。。。

ということになってしまいます。




質の高いテストコードの特徴その3:迅速なフィードバック

プロダクションコードの変更〜フィードバックを得る〜改善までが遅れれば、

手戻りにかかる時間が無駄に多くなってしまいます。

そのためテストが迅速にフィードバックできることは重要です。




実際に自分はテストコードを流すのに1時間近くかかるような現場にいましたが、

テストコードが通らないとソースコードがdevelopブランチにマージできず、

開発効率が非常に悪かったです。




先に他のメンバーにマージされてしまうと、

また、それを取り込んでテストコード流しなおしたり・・・

テストが回ってる最中に待ちきれずに次のコードを書き始めるも、

後からテスト結果が出て手戻りがあることに気づいたり・・・




質の高いテストコードの特徴その4:保守のしやすさ

テストの保守のしやすさは

  • テスト・ケースを理解することがどれくらい難しいのか??
  • テストを行うことがどれくらい難しいのか??

の2つ観点で確認できます。

テストコードが短く、何を検証したいのかパッとわかるテストケースは保守しやすいです。




また、テスト対象が

データベースや他サービスなどの外部プロセスへの依存しておらず、

テストコードにモックの準備など複雑な準備フェーズがない方が保守しやすいです。




テストコードに4つの特徴をどういうバランスで備えさせるか

ここまで見てきた4つの特徴の掛け合わせで

単体テストの価値が決まります。

どれか1つでも全く満たせないと(0があると)

単体テストの価値は0になってしまいます。




一方で、

退行に対する保護、リファクタリングへの耐性、迅速なフィードバックは

互いに相反する性質のため同時に2つまでなら最大限満たせるものの、

3つ同時に最大限満たすことはできません。

(保守のしやすさは他の特徴と違って全ての単体テストが備えるべき特徴です)




もしどれかの特徴が満たせなければ・・・

テストコードはこんな感じになります。


テストコードの4つの特徴のどれか満たせないと・・・


また、同時に2つ最大限満たせるうちの1つは、

リファクタリングへの耐性を最大限満たす必要があります。

リファクタリングへの耐性を少しだけ満たすことはできず、

リファクタリングへの耐性を満たしているか満たしていないかの2択になるためです。




で、そうなると退行に対する保護と、

迅速なフィードバックをどのようにバランスを取っていくのが良いのでしょう??




テストピラミッドの各層でのバランスの取り方は、

以下をまずは基本として考えるのが良いと自分は考えています。


テストピラミッドの各層でのテストの特徴のバランス


誤解のないように補足すると・・・

小としているものは、なくて良いというわけではなく、

それ以外の特徴を最大にするように優先度を落とす方針を基本方針とする。

と捉えていただければと思います。




テストピラミッドの各層に対してなぜこの基本方針にすると良いかというと、

まずピラミッドの高い層であるE2Eテストが最もユーザ体験に最も近いテストであり、

データベースや外部サービスなどのプロセス外依存を全て本物を使ってテストします。

プロセス外依存を全て本物を利用するのはテスト実行に時間がかかり、

どうしても迅速なフィードバックを最大にすることはできないです。




一方で、

ピラミッドの低い層ほどテストケース数は多くなるので、

迅速なフィードバックが他の層よりも優先する必要があります。

また、

上の層と比較すると、

ユーザの体験から離れた、より実装の詳細に近いテスト対象へのテストになります。

実装の詳細に近いテスト対象へのテストでは、

退行に対する保護を最大限備えることはどうしてもできません。




繰り返しになりますが、4つの特徴のどれかがゼロだと

そのテストコード自体の価値がゼロになってしまうので、

優先度をつけながら全ての特徴を備えさせるのが大前提です。

(ビジネスロジックをほとんど持たないシステムがテスト対象の場合など

極端な場合などこの基本方針が使えない場合もありますが・・・

ややこしいので一旦そこには触れません)




最後に、

自分は統合テストと単体テストのテストコードを書くことが現場では多いのですが、

その際以下のようなことに注意するようにしています。

  • このテストコードは統合テストなのか??単体テストなのか??
  • 単体テストが書きたくても統合テスト書いてしまっていないか??(設計見直しが必要)
  • 退行に対する保護にならない単体テストコードを形だけ書いてないか??
  • 最終的にはテストスイート全体でバランスを取る(1テストケースだけで4つの特徴全て完璧には満たせないのだから)


参考文献

コメントを残す

メールアドレスが公開されることはありません。 が付いている欄は必須項目です

このサイトはスパムを低減するために Akismet を使っています。コメントデータの処理方法の詳細はこちらをご覧ください