はじめに
今回は、
質の高いテストコードが備えるべき4つの特徴と
それらのバランスの取り方について、
まとめました。
4つの特徴のうち3つは相反し、
さらに4つの特徴のうち1つでもゼロ(全く特徴を備えない)
があるとテストコードの価値がなくなるので注意が必要です。
動画にもまとめたので是非ご覧ください。
質の高いテストコードが備えるべき特徴
質の高いテストコードの特徴その1:退行(regression)に対する保護
プロダクションコードを変更した際、
元々動いていた既存機能が動かなくなるような退行に対して、
保護(検知)する仕組みは重要です。
その退行に対する検知の精度が高いほど質の高いテストコードといえます。
退行に対する保護がテストコードにどれくらい備わっているかを把握するには、
- テストコード実行時に多くのプロダクション・コードを実行できているか??
- テストコードで実行されるプロダクション・コードは、ビジネス的に重要で複雑なコードか??
これらの観点でテストコードとテスト対象を分析する必要があります。
質の高いテストコードの特徴その2:リファクタリングへの耐性
プロダクションコードにはリファクタリングを頻繁に行うことで、
無秩序なコードを整理して開発スピードを保てるようにすることが重要です。
プロダクションコードの振る舞いを変えることなく、
実装の詳細を変更するこのリファクタリングを行った場合、
テストが失敗してはなりません。
もし、テストが失敗するようになるのであれば、
テスト対象の機能が正しく振る舞っていることを
正しく検証できていなかったということになります。
(偽陽性:false positive)
リファクタリングへの耐性が備わっていなければ、
それは質の高いテストコードとはいえません。
実際に僕がいた現場での例ですが、
テストコードがあるのに、
テスト対象の実装の詳細を細かく検証しすぎたせいで、
少し実装の詳細をリファクタリングしただけで、
テスト結果がほとんどNGになる。
(外から見た振る舞いは変更していないので本当はテスト結果OKになるべき)
そのため、
リファクタリング時にはセットでテストコードを修正する必要があり、
テストコードがリファクタリングの足枷になる。
といった体験をしたことがあります。
これがひどくなると、、、
次第に開発者はテスト結果が信頼できずテストコードを無視するようになり、
結果として問題のあるコードが本番に持ち込まれるようにもなります。
テストコードにリファクタリングの耐性がなければ、
安心してリファクタリングを行うため、
工数をかけて作成していたはずのテストコードが、
いつの間にか、リファクタリングの足枷になってきて、
テストコードがない時と状況が大して変わらない。。。
ということに陥ります。
質の高いテストコードの特徴その3:迅速なフィードバック
プロダクションコードの変更〜フィードバックを得る〜改善
までが遅れれば、
手戻りにかかる時間が多くなります。
そのためテストが迅速にフィードバックできることは重要です。
自分はテストコード実行に1時間近くかかるような現場にいましたが、
テストコードが通らないとソースコードがdevelopブランチにマージできず、
開発効率が非常に悪かったです。
先に他のメンバーにマージされてしまうと、
また、それを取り込んでテストコード流しなおしたり・・・
テストが回ってる最中に待ちきれずに次のコードを書き始めるも、
後からテスト結果が出て手戻りがあることに気づいたり・・・
質の高いテストコードの特徴その4:保守のしやすさ
テストの保守のしやすさは
- テスト・ケースを理解することがどれくらい難しいのか??
- テストを行うことがどれくらい難しいのか??
の2つ観点で確認できます。
テストコードが短く、何を検証したいのかが
パッとわかるテストケースは保守しやすいです。
また、テスト対象が
データベースや他サービスなどの外部プロセスへの依存しておらず、
テストコードにモックの準備など複雑な準備フェーズがない方が保守しやすいです。
テストコードに4つの特徴をどういうバランスで備えさせるか
ここまで見てきた4つの特徴の掛け算で
単体テストの価値が決まります。
つまり、
どれか1つでも全く満たせないと(0があると)
単体テストの価値は0になります。
一方で、
退行に対する保護、リファクタリングへの耐性、迅速なフィードバックは
互いに相反する性質のため3つ同時に最大にはできません。
(保守のしやすさだけは全ての単体テストに備えさせられる)
もしどれかの特徴が満たせなければ・・・
テストコードはこんな感じになります。
テストケースのバランスの取り方ですが、
リファクタリングへの耐性は少し備えるなどができないため、
最大限満たしつつ、
退行に対する保護と、迅速なフィードバック
のバランスを取ることになります。
1つのテストケースにおいて全ての特徴を備えることができないため、
テストスイート全体でバランスを取ることが大事だと僕は考えており、
図のように、
テストピラミッドの各層でバランスの取り方変えるのを
現場では基本方針としています。
この方針にする理由を少し補足すると、
まずE2Eテストについてですが、
最もユーザ体験に最も近く(実装の詳細からは遠いため退行に対する保護は高められる)、
データベースや外部サービスなどは基本的に全て本物を使うため
テスト実行に時間がかかります(迅速なフィードバックは難しい)
逆に単体テストですが、
テストケース数が多く、
迅速なフィードバックが求められますが、
(データベースなど利用しないため迅速なフィードバックは高められる)
ユーザの体験から離れた、より実装の詳細に近いテストになります。
(実装の詳細に近く退行に対する保護は低くなる)
間の統合テストは、
迅速なフィードバックを高めても、
退行に対する保護を高めても良いと考えます。
どちらかというと退行に対する保護がより高めるのをを意識しておいて、
実行時間が問題になるようになれば迅速なフィードバックも高める。
くらいの感覚で自分はいます。
繰り返しになりますが、4つの特徴のどれかがゼロだと
そのテストコード自体の価値がゼロになってしまうので、
優先度をつけながら全ての特徴を備えさせるのが大前提です。
(ビジネスロジックをほとんど持たないシステムがテスト対象の場合など
極端な場合などこの基本方針は通用しませんが・・・そこは置いておきます)
ちょっとおまけで・・・
自分はテストコードを書く際はこの辺りを注意するようにしています。
- このテストコードは統合テストなのか??単体テストなのか??(どの基本方針を採用するか確認)
- 単体テストが書きたくても統合テスト書いてしまっていないか??(設計見直しが必要)
- 退行に対する保護にならない単体テストコードを形だけ書いてないか??(削除する)
- 最終的にはテストスイート全体でバランスを取れば良い(1テストケースだけで4つの特徴全て完璧には満たせない)
今回のテーマについては以上です。
テストコード初めての方へ向けて記事を書いたので
是非こちらもご覧ください。
初めてテストコードを書く方に向けて|新人プログラマー時代の自分に伝えたいこと参考文献
第4章 良い単体テストを構成する4本の柱
単体テストの考え方/使い方