品質

【書評】良い単体テストってどういう状態か?【単体テストの考え方/使い方】

2024年3月2日

「単体テストの考え方/使い方」を読んで、「良い単体テストとは、どういう状態か?」についてポイントをまとめた記事です。

 

ちなみにここでいう単体テストは「コードで書かれた単体テストで、CIツール等用いて自動化できるもの」を指しています。

本書を読むメリット

「低コストでバグを検知してくれるようになる単体テストを書くための知識が身に付く」ことが、本書の最大のメリットだと思います。

エンジニアの業務上、「プロダクションコードと一緒に、それを検証する単体テストも書いてPullRequestを出す」パターンが多いですよね。

(完全に一緒とは言わずとも、近いタイミングで単体テストを追加するんじゃないかと思います)

そういう意味で、単体テストは「最も日々の業務に近い」知識領域になるため、本書を読んでセオリーを学ぶことによるコストパフォーマンスがかなり大きいと感じています。

想定読者

  • テストライブラリ(JUnit,XUnitとか)を使って単体テストを書いたことがある人
  • テストコード自体が保守しづらくなってきている実感のある人
  • サクッとバグを検知してくれるテストコードを書きたい人

このような方に当てはまる書籍となっています!

ちなみに、「テストコード自体書くのが初めてだよ」という方は、まず単体テストを書いて、それを自動化する活動を自分でやってみてから読んだ方がより学びが深いと思います!

以下、本題に入っていきます。

そもそも単体テストって何でやるのか

書籍によると

ソフトウェア開発プロジェクトの成長を持続可能なものにするため

とあります。

さらに具体化すると「新規機能追加やリファクタリングの際に既存機能が動かなくなる事態(=退行,デグレ)を防ぐため」となります。

単体テストは「既存機能が動くかどうか」を監視するセーフティネットとして配置しておくものなんですね。

単体テストの4つの基準

では、単体テストがうまく機能しているかどうかを判断するには、どこを見れば良いのでしょうか。

本書では以下の4つが「単体テストの4つの基準」として紹介されています。

1.退行(デグレ)に気付けるか

単体テストの本分である「既存機能が動くかどうか」を教えてくれる要素のことです。

新機能を追加した時にNGを検知し、そのままリリースすると既存機能が動かなくなることを教えてくれます。

2.リファクタリングへの耐性があるか

テスト対象の振る舞い(どういうInputに対してどういうOutputがあるか)を変えることなく、内部のロジックを保守しやすいように変えることをリファクタリングと言います。

その際に

  • テストの結果がOKになるべきなのにNGになってしまう(偽陽性)
  • テストの結果がNGになるべきなのにOKになってしまう(偽陰性)

といった困った事象にならないか、と言う観点が「リファクタリングへの耐性があるか」です。

※陽性or陰性の定義がややこしいですが、「テストがNGになる=チェックに引っかかる=陽性」なので、これで合ってます。
「コロナウイルス陽性でした」とか言う時も、ある意味「テストの結果がNG」ですよね。

偽陰性と偽陽性で何が困るか

偽陰性が起きると、開発者が埋め込まれたバグに気づけず、バグったコードが本番環境にリリースされます。困りますね。

一方で偽陽性の発生頻度が多くなると、

「このテスト、仕様通りに動くコード書いてもNG出してくるな... 今回もNG出てるけど無視してマージしたろ!!!」

といった感じで開発メンバーが徐々にテスト結果を信頼しなくなっていき、もし本当にバグが埋め込まれた時にもスルーしてしまう「オオカミ少年」状態に近づきます。

3. テスト結果が早く分かるか

そのままの意味です。

単体テストを書いたらGithub Actions等のCIツールを使って定期実行すると思いますが、

「1行コードを変えただけなのに、それを検証するためのテストに1時間かかる」とかだと、非常に萎えます。

待たされている間何か別作業をするにしても、1つのタスクに対する集中力が途切れるので、全体として開発生産性が落ちます。

現場の肌感ですが、10分以上かかると「遅いな〜」と感じ始める感じです。

4. 保守しやすいか

これもそのままの意味です。

  • 後から読んで何をテストしているのか分かりやすい
  • テストを実行するために必要な準備が少ない

とよりGoodです。

これは持論ですが、テストコードファイルは「仕様の説明書」としての役割も一部あると思っています。

何をテストしているのか、テスト対象はどんなふうに動くのかがテストコードから読み取れると最高ですね。

4つの要素をどう追求するか

ここまで書いてきた4つの要素を、全て完璧に備えたコードは、実は存在しません。

物事はトレードオフなので、バランスを考える必要があります。

テストピラミッド

本書では、自動テストは下記のように大きく3つの分類に分かれるとされています。(テストピラミッドとも呼ばれます)

テスト分類 役割
単体テスト 1単位の振る舞いを検証するテスト
統合テスト 単体テストに加えて、外部依存先(DB/ファイルシステム/Http通信など)も含めた振る舞いを検証するテスト
E2Eテスト エンドユーザの視点でシステムを検証するテスト。UIテスト/GUIテスト/機能テストとも呼ばれる
テストピラミッドの図

バランスをどう取るか

4つのポイントについて、単体テスト/統合テスト/E2Eテストそれぞれの重要度は以下のようになります。

各テストフェーズにおける4つの要素の重要度

  • 保守しやすさはどのフェーズでも上げておく必要がある。テスト対象といえど、変更対象である以上はプロダクションコードと同じく、読みやすく,変更しやすくしておきたい。
  • リファクタへの耐性もどのフェーズでも上げておく必要がある。この場合の「リファクタ」は、各テストフェーズにおいてどの観点でテストしているかに依存する。具体的には
    • E2Eテストフェーズでは、「システムをエンドユーザから見たときの挙動を変えない」リファクタ時に、無闇にテストがNGにならないようにテストを書いておきたい。
    • 統合テストフェーズでは、「外部依存先(DB/ファイルシステム/Http通信など)も含めた振る舞いを変えない」リファクタ時に、無闇にテストがNGにならないようにテストを書いておきたい。
    • 単体テストフェーズでは、「ある1つの部品の振る舞いを変えない」リファクタ時に、無闇にテストがNGにならないようにテストを書いておきたい。
  • 結果が分かる早さは、単体テストで最も重要であり、E2Eテストでは重要視されない。E2Eが早いに越したことはないが、そもそも時間がかかるものなので、仕方ない
  • デグレへの耐性は、E2Eテストで最も重要であり、単体テストでは重要視されない。デグレ=「ユーザから見たバグ」のため、ユーザ目線でのテストであるE2Eで最も重要視される。

また、一つでも要素が0になると、その単体テストの価値も0になります。

まとめ

以上、「良い単体テストとは?」を4つの要素からまとめました。

これらの知識を現場に合わせて組み合わせつつ、テストを改善していき、品質改善に繋げたいですね。

長くなるので本記事では触れられませんでしたが、現場あるあるの紹介をしつつ、テストコードの改善事例紹介記事を執筆中です。お楽しみに!

-品質