🕌

単体テストの考え方/使い方 第1部を読んで

2023/08/13に公開

昨年度末に出版された「単体テストの考え方/使い方」の第1部を読み、その要点を自分用にまとめました

単体テストはなぜやるのか

  • プロジェクトを「持続可能」なものにするため

    • プロジェクトは成長するごとに費やす時間が増えていくが、質のいいテストがあることで費やす時間を減らすことができる
    • ただし、テストがあればいいというわけではない
    • 単体テストの「価値」と「保守にかかるコスト」に注意すること
    • 保守コストが過度にかかると、テストの価値が低下してしまう
    保守にかかるコストとは?

    以下の作業に費やされる時間が積み重なるごとに上昇していく

    • プロダクションコードのリファクタリングに伴ってテストコードもリファクタリングをすること
    • プロダクションコードの変更毎にテストを実施すること
    • テストが失敗した際にその対処をすること
    • プロダクションコードの振る舞いを確認するためにテストコードを確認すること
  • カバレッジとテストの関係

    • カバレッジが高くてもテストの質が高いわけではない
      • よく言われている言葉だが、額縁に入れて飾りたい言葉
    • 以下の2点を考慮すると、カバレッジが上がったからといって、テストの質が必ずしも上がったとは言えない
      1. カバレッジからは実際にテスト対象コードが検証されたのかを保証できない
      2. カバレッジ算出の際にライブラリ内のコードは計算から外れる
    • 特に1については、検証をしなくてもメソッドの実行を行うだけでも上げることができてしまう
    • そのため、カバレッジの値を良くするのはいいことだが、それを強要するとテストの本質から離れてしまう

単体テストとはなにか

  • 単体テストの定義
    1. 自動化されている
    2. 「単体(unit)」と呼ばれる少量のコードを検証する
    3. 実行時間が短い
    4. 隔離された状態で実行される
  • 単体テストの派閥には「古典学派」と「ロンドン学派」の2つが存在する
    • 今回は本の作者が賛成している「古典学派」についてのみ記述する
  • 「古典学派」の考える単体テスト
    1. 1つのテストケースが検証するのは1単位の振る舞いであり、必ずしもクラスが1つとは限らない
      • ちなみに「ロンドン学派」はテストケース1つにつき検証するのは1つのクラスであり、関連する依存は全てテストダブルに置き換える
    2. テストケースがほかのテストケースに影響を与えないこと
    3. 共有依存(テストケース間で共有される依存、データベースやファイルシステム、staticな可変フィールドなど)をテストダブルに置き換えること
    • 以上の方針に従って単体テストを作成する

単体テストの構造

  • AAAパターンを利用する
AAAパターンとは

準備(Arrange), 実行(Act), 確認(Assert)の3つのフェーズで構成されたテストの書き方

準備(Arrange)フェーズ

- テストケースの事前条件を満たすようにテスト対象システム(System Under Test: SUT)とその依存の状態を設定するフェーズ

実行(Act)フェーズ

- テスト対象システムのメソッドを呼び出すことでテスト対象の振る舞いを実行させるフェーズ。
- もし実行結果が返ってくるのであれば、それを確認する時に使えるように保持しておく

確認(Assert)フェーズ

- 実行結果が想定していた結果であることを確認するフェーズ
- 実行結果が戻り値のこともあれば、テスト対象システムや協力者オブジェクトの状態、または協力者オブジェクトのメソッドが呼び出されたことが確認結果になることもある

単体テストにおいて回避すること

  • 1つのテストケース内で同じフェーズを繰り返すこと
    • 準備→実行→確認→実行→確認…みたいな
    • 複数の振る舞いを確認するのは統合テストとなる、単体テストの場合は1つの振る舞いを確認するようにする
    • 分割して複数のテストケースにするとよい
    • テストケースの理解のしやすさや保守のことを考えると1テスト1フェーズが好ましい
  • if文の使用
    • if文があるということはその分岐自体が別のテストケースとなる
    • テストケースに分岐を持ち込むと読みにくく、テストの複雑性を増加させることになって保守コストがかかるようになる

各フェーズについて

  • 各フェーズのサイズはどのくらいが適切か
    • 準備フェーズが最も大きくなる
    • 大きくなりすぎる場合はプライベートメソッドにしたり、ファクトリクラスをつくったりするとよい
      • テストケース間で準備フェーズのコードを共有することに関する有用なパターンとしてオブジェクトマザー、テストデータビルダーが挙げられる
  • 実行フェーズのコードが1行を超える場合は注意
    • 実行フェーズが2行以上になるということは1つの振る舞いで複数のメソッドを呼び出す必要があるということ
    • つまり、カプセル化が行えておらず、データの整合性が失われる可能性がある
    • 2行以上呼び出すなとは言わないが、カプセル化の破綻が起きていないかを考える
  • 確認フェーズで確認する項目はどれくらいあると良い?
    • 多すぎなければ問題なし
    • オブジェクトの全フィールドを確認したりしているなら返ってきたオブジェクトと期待するオブジェクトを比較できる仕組みを作るべき
  • テストの後始末はどうする?
    • 例えば、作成したファイルを削除したり、DBとの接続を切ったりなど
    • そもそも多くの単体テストでは、プロセス外依存とのやりとりをしないのでほぼ必要ない
    • また、後始末は基本的に別のメソッドで用意できることが多い
  • テスト対象システムとその依存との区別
    • テストコードを見た時にぱっと分からないことがある
    • そのような場合はテスト対象システムに「sut」という変数名を使うと良い(System Under Test)
  • テストケースから各フェーズを示すコメントを消す
    • もしAAAパターンを採用したテストケースにおいて、各フェーズを書くのに空白行が不要であれば、空白行で各フェーズを区切るようにする
    • そうでない場合は各フェーズの先頭にコメントをつける

単体テストの名前の付け方

最も役に立たない命名規則

  • {検証するメソッド}_{前提条件}_{想定する結果}
    • 例えば2つの値を返すメソッドなら
    • sum_twoNumbers_returnSum と分かりにくくなる
    • これならsum_of_two_numbers でいい
  • テストは開発者のみでなく非開発者でも読めるようにするとともに、開発者に認知的負荷をかけないようにする
  • そのテストが何を行うのかが中身を見ないとわからない命名は保守コストの増大につながる

テストメソッドに名前をつけるときの指針

  • 次の指針を守ると読みやすくなる
    1. 厳密な命名規則に縛られないようにする
      • 厳密な命名規則で表現できることは限界があるので自由に命名できるようにする
    2. 問題領域に精通している非開発者に対してどのような検証をするのかわかるような名前にする
    3. _を使って単語を区切る
      • 長い名前だと読みやすくなる、まぁこれは別にいいかなと思う
    4. テスト対象の名前をテストメソッドに含まない
      • 単体テストは1つの振る舞いをテストしているため、テスト対象の名前を入れるとコードと紐づいてしまう
    5. should(べきである)を使わない
      • 単体テストは1つの事実(シナリオ)を伝えるものなので、shouldではなくisをつかうようにする
  • とは言っても、チーム内でどのような名前にするかの方針があれば、それに従うようにする

以上です
自分の中で、なんとなくわかってはいるが言語化をできていなかった部分を明確にできた気がします

Discussion