この記事は自分の書いているユニットテストが良いかどうかが分からない、ユニットテストを効果的に利用できていないがどう改善して良いか分からない、という方向けとなります。
良いユニットテストとは何か
プロダクションコードと同様に、ユニットテストのコードも正しく評価し、改善していく必要があります。
ユニットテストの品質が低いと、プロダクションコードの進化の足を引っ張り、開発速度の低下を招くためです。
では、ユニットテストをどう評価し、どう改善していくべきでしょうか?
二つの主要な評価軸
混入したバグの発見とリファクタリングへの耐性が2つの主要な評価軸になります。
混入したバグの発見能力
ユニットテストを作成してテストを自動化するのであれば、混入したバグの発見が目的となります。
当然、バグを正しく発見しテストを不合格にする必要があります。
これは自然な目的であり、異論が無いかと思います。
しかし、ユニットテストが合格している=バグが無い、というわけではない点に注意が必要です。
以下の点に注意してユニットテストを作成する必要があります。
- 一度は意図的にユニットテストを不合格にし、正しくバグを発見できることを確認する
- テストファーストで作るかプロダクションコードに意図的にバグを入れる等する
- 戻り値だけでなく副作用を確認する
- DB更新等の外部システムへの変更があれば確認する
- 状態(State)の変化があれば確認する
リファクタリングへの耐性
プロダクションコードを継続的に進化させていくためには、リファクタリングが必須となります。
リファクタリング中はコードにわずかな修正を入れるごとにユニットテストを実行し、
バグが混入していないことを確認しながら作業を進める必要があります。
この時、外部から見える動作を変更せずに、実装の詳細に当たる部分を修正した場合に、
ユニットテストは正しく合格する必要があります。
これがリファクタリングへの耐性です。
もしこのようなリファクタリングでユニットテストが不合格になる場合、
そのユニットテストはリファクタリングを助けてくれません。
このようなユニットテストは改善するべきです。
両者のバランス
リファクタリングへの耐性を保ちながら混入したバグの発見を行えるようにするべきですが、
極端にカバレッジを重視してprivateメソッドをMockライブラリで差し替えるようなテストを行うと、
リファクタリングへの耐性が無くなってしまいます。
この状態はユニットテストの価値が下がるだけでなく、メンテナンスコストが極端に高くなりますので、避けるべきです。
ユニットテストはあくまで外部から見える動作である、クラス/メソッドの契約内容に対してテストするべきです。
この契約内容について網羅的にテストを行うことで、バグの発見とリファクタリングへの耐性を両立することができます。
インタフェースによる外部依存の切り離し
リファクタリングへの耐性を重視してDB、通信等の外部依存をユニットテストでそのまま使用すると、
実行速度の低下が懸念されます。
しかし、外部依存のAPIを直接モック化するとリファクタリングへの耐性が損なわれます。
リファクタリングへの耐性と実行速度を両立するには、外部依存に対してインタフェースを挟み、処理を切り離しておく必要があります。
そして、そのインタフェースへの呼び出しをクラス/メソッドの契約として整理します。
この方法で、外部依存に対してリファクタリングの耐性を保ったままモックに差し替えることができます。
コードの契約については、別の記事でより詳しく掘り下げようと思います。
リファクタリングへの耐性の重要度
リファクタリングへの耐性は開発初期にはさほど重要視されません。
開発初期はリファクタリングを行う機会が少なく、またリファクタリング中にユニットテストが誤って不合格になってもわずかな労力で修正できるためです。
しかし、開発が進みプロダクションコードとユニットテストが複雑化してくるとリファクタリングの耐性の重要度が一気に高まります。
プロダクションコードの継続的な進化にはリファクタリングが必須ですが、その時にユニットテストが誤って不合格になるとリファクタリングを進めることができません。
当然ユニットテストの修正が必要になりますが、開発が進んだ状態でユニットテストを修正するのはリスクが伴います。
ユニットテスト側にバグを混入させ、プロダクションコードのバグを発見できなくなる可能性があるためです。
このような状況を避けるために、開発の初期からリファクタリングへの耐性は意識して維持しておく必要があります。
テストコードの可読性
テストコードの可読性はプロダクションコードの可読性とは評価方法が異なります。
重要なのは以下の二点です。
- 何をテストしているかを理解しやすいこと
- シンプルな処理で記載されていて、バグ混入の可能性が低いこと
テストコードはこれらの特性を満たすために、あえて冗長な書き方をすることもあります。
AAAパターン
テストケースに対して、以下のフェーズで構成することをAAAパターンと呼びます。
- 準備(Arrange)
- 実行(Act)
- 確認(Assert)
各テストケースを一貫した構成で記載することで、何をテストしているかを理解しやすくなります。
DRYかDAMPか
テストコードではDRYにこだわるよりも、
DAMP(Descriptive and Meaningful Phrases:冗長でも意味が明確であること)を受け入れる必要があります。
もちろん、全テストケースの準備(Arrange)でほぼ同じ処理をで行っている場合などに、
メソッド化することでDRYと可読性を両立できるケースもありますが、過度にDRYを遵守するよりも、DAMPを受け入れるべきです。
分岐、ループの回避
テストコードに分岐(if文等)、ループを入れるのは慎重になるべきです。
これは、テストコードの可読性が下がることと、テストコードに不具合を混入させる可能性があるためです。
テストコードはクラス/メソッドが契約に従った動作をしていることを宣言的に記載するべきです。
まとめ
ユニットテストも正しく評価し、ユニットテストと合わせて進化させていく必要があります。
- 混入したバグの発見能力とリファクタリングへの耐性のバランス
- リファクタリングへの耐性を保ちながら混入したバグの発見能力を維持する
- リファクタリングへの耐性は開発が進むと重要度が増す
- インタフェースを使用することで契約を整理し、リファクタリングへの耐性とテストの実行速度を両立できる
- ユニットテスト特有の可読性に注意する
- 基本の書き方はAAA
- DRYにこだわらずDAMPを受け入れる
- ユニットテスト内に分岐、ループ等の制御構造はなるべく持ち込まない
良いユニットテストはプロダクションコードの進化速度を速めることができます。
この速度向上はテストコードを書くのにかかる時間を投資するのに十分なものです。
この記事が、あなたのユニットテストの品質向上と、プロダクションコードの開発速度向上につながれば幸いです。