📖

単体テストの考え方/使い方 超要約(第1〜2部)

2023/12/27に公開

「単体テストの考え方/使い方」を読みながらまとめた超要約(意訳)メモです

超要約ゆえセクションタイトルも本物とは違いますのでご承知おきください

https://www.amazon.co.jp/dp/4839981728

第1部 単体テストの概観

第1章 Why

  • 退行(regression)を防ぐために単体テストを書く
    • 開発サイクルの中に単体テスト実行が入っていなければ、書く意味がない
  • 網羅率は「テストの質が低いこと」は評価できるが、「テストが完璧であるか」の評価には全く使えない
    • 小手先のテクニックで網羅率は簡単に上げることができてしまうので
  • テストコードは多ければいいってもんじゃない
    • テストコードを書いたりメンテしたりすることにはもちろんコストがかかる
    • コストよりも価値の方が高くなるようなテストコードのみを残すべき
      • プロジェクト内の重要な箇所のテストだけ書く
      • 価値の高いテストコードを書く
    • テストコードは資産ではなく負債として見るべき

第2章 What

  • 単体テストの「単体」とは何か
    • ざっくり古典学派とロンドン学派の二大派閥がある
      • 古典学派
        • (ブラックボックス的観点での)「一単位の振る舞い」を単体と考え、他のテストケースに干渉しないことを大切にする
      • ロンドン学派
        • 「クラス」を単体と考え、他クラスへの依存はテストダブル(モック)によって断ち切るべきと考える
  • 一見ロンドン学派の方がしっかりしてて良さそうだが、実際は古典学派の方がいいと思うよ
    • ロンドン学派は粒度がかなり細かいので、故障時に原因を特定しやすい
      • ただし、テストが細かすぎて(プロダクションコードに密接に結びついた内容になりすぎて)過剰になってしまいがち
      • プロダクションコードをいじるたびに(CI等で)テストを動かすようにしていれば、古典学派的アプローチであっても故障箇所の特定は難しくない(いじった箇所に問題があるとみなせるので)
    • 大事なのは一単位の振る舞いがちゃんとしていること
      • テストケースが書きにくいのであれば、そもそも設計に問題がある可能性が高い

第3章 How

  • AAAパターンがいいぞ
    • Arrange 準備
    • Act 実行
    • Assert 確認
  • テストコードを見やすくしよう
    • フェーズ名をコメントで書くとわかりやすいね
      • 各フェーズが1行で終わるくらいシンプルだったら、フェーズ間に空行を挟むだけで十分かもだけど
    • テスト対象にsutsystem under test テスト対象システム)と名付けるとわかりやすいよ
fun `なにかのテスト`() {
    // 準備
    val hoge = Hoge()
    val sut = Fuga()
    
    // 実行
    sut.doFugaFuga(hoge)
    
    // 確認
    ...
}
  • 一つのテストケース内に同じフェーズが二つ以上出てきてはいけない
    • テストケースを分割せよ
  • 準備フェーズは長くなりがち
    • テストクラス内にファクトリメソッドを作ってテストケース間で共通化するとすっきりするかも
private fun CreateProduct(name: String, taxExPrice: Int): Product {
    return Product(
        name = name,
	priceInfo = PriceInfo(taxExPrice = taxExPrice),
    )
}
  • 実行フェーズが複数行になっちゃうときは、テスト対象メソッドの設計に問題があるかも
    • 使う側はそれ一つだけを呼び出せばいい、という作りになっているのが望ましいはずなので
  • テストメソッド名は、非開発者でも目的がわかるような名前にせよ
    • テスト対象メソッド名はつけちゃだめだよ
      • テストケースはテスト対象メソッドに依存するのではなく、「一単位の振る舞い」に依存すべきなので
  • parameterizedTestはいいぞ
    • インプットだけが少し違う複数のテストケースを一つのテストメソッドにまとめられる
      • 境界値のテスト等で便利そう
    • ただしまとめることにより意図が不明瞭になりやすくなってしまうので、まとめすぎ注意

第2部 価値のある単体テストをどう書くか

第4章 よいテストとは

  • よいテストは4つの柱の上に立つ
    • 退行に対する保護
      • プロダクションコードにバグが混入しそうになったときにしっかり検出するということ
    • リファクタリングへの耐性
      • プロダクションコードをいじったときに、テストコードの修正が不要、または少なく済むこと
    • 迅速なフィードバック
      • すぐに気軽にテストを実行できること
    • 保守のしやすさ
      • テストコードが読みやすいこと
  • リファクタリングへの耐性は必須
    • これがないと、テスト結果が偽陽性(正しいプロダクションコードを書いてるのにテストNG)になり、開発者がテストをアテにしなくなってしまうので
  • 「退行に対する保護」と「迅速なフィードバック」を同時に最大化することはできない
    • テストの種類により、ウェイトの置き方を変えよう
    • テストピラミッドの上層では「退行に対する保護」寄り、下層では「迅速なフィードバック」寄りにしよう
      • 上層:E2Eテスト
      • 中層:統合テスト
      • 下層:単体テスト
  • テストはホワイトボックス観点でケースを切り分け、ブラックボックス観点で作るのがいい

第5章 モック

  • テストダブルには大きく分けて二つあるよ
    • モック
      • 「外部を操作するためのコミュニケーション」を置き換えるもの
        • 呼び出し先のシステムの状態を変化させるなど DB操作のupdate的な
      • モックかつスタブなものはモックと考えていいよ
    • スタブ
      • 「外部からデータをもらうコミュニケーション」を置き換えるもの
        • 呼び出し先のシステムの状態は変わらない DB操作のselect的な
  • スタブの仕事は検証するものじゃない
    • 一単位の振る舞い(どんなインプットに対しどんな結果が得られるか)を検証する上で、スタブの仕事内容はどうでもいい
    • スタブの仕事内容を検証するようにしてしまうと、テストが壊れやすくなる
    • あくまでスタブの役割は模倣(いいかんじのレスポンスを返してくれる存在)
  • モックの仕事(成果)は検証する必要がある
    • 「一単位の振る舞い」の結果の一つなはずなので
  • そもそも、プロダクションコードのAPIを適切にしよう
    • 「観察可能な振る舞い」のみがpublicメソッドとして公開されているのがいい
      • そして、これこそをテストで検証する
    • それ以外(実装の詳細)はprivateメソッドとして隠蔽せよ

第6章 テストしやすい実装を考える

  • 単体テストは以下の3種類に分類できる
    • 出力値ベーステスト
      • 入力aに対しては出力f(a)が得られるよね、ということを見るテスト
      • 暗黙的な入出力がない
        • 処理内でDBを参照するとか、クラスの状態を更新するとか、そういうのがないよという意味
      • よいテストの4つの柱を最も保ちやすい
      • 状態ベーステスト
        • 実行後にクラスのメンバ変数とかがこういう状態になるよね、ということを見るテスト
      • コミュニケーションベーステスト
        • この処理を行う過程でアイツがちゃんと呼び出されるよね、ということを見るテスト
  • 出力値ベーステストが一番うれしいので、可能な限りこれにしたいね
    • そんな理念で生まれた?のが関数型プログラミング
      • 数学的関数(純粋関数)、つまり、入力aに対して出力f(a)を返し、それ以上のことは起こらないよ、という関数・メソッドを意識的に作っていこうという戦略
      • 具体的には、ビジネスロジック(この値とこの値を参照して、こうだったらこういう結果になる!というようなロジック)を数学的関数にして、副作用(DBを更新するとか)ときっちり分離しようという話
        • 関数型アーキテクチャは保守がしやすいが、システムのパフォーマンスが下がってしまうので、銀の弾丸ではないよ
        • 複雑で重量なシステムには関数型アーキテクチャがいいかもね

第7章 テストしやすい実装の作り方

  • 第1章でも触れた通り、テストコードは多ければいいってもんじゃない
    • そこで、プロダクションコードを「テストすべき箇所」と「テストしなくていい箇所」に適切に分離していきたい
  • テスト作成という観点では、プロダクションコードを四象限に区分することができる
  • 各軸について
    • x軸
      • 協力者オブジェクト
        • 簡単に言えば、(テスト対象のクラスが)依存するクラス
    • y軸
      • コードの複雑さ
        • 簡単に言えば、分岐の数
        • 自分で明示的に書いたコードだけでなく、利用するライブラリにも暗黙的に存在し得るので注意
      • ドメインにおける重要性
        • ビジネスにおいて必要不可欠な機能
        • コードが複雑な箇所はドメインにおいて重要がちだけど、必ずしもそうではない(ので、「コードの複雑さ」と等価ではない)
  • 各象限について
    • テストしたいが困難なコード
      • 勢いだけで実装すると大体これになってしまう
      • これを可能な限り分解して「単体テストの価値が高いコード」「統合テストの価値が高いコード」に近づけていきたい
    • 単体テストの価値が高いコード
      • 純粋なドメインクラス、ビジネスロジック
    • 統合テストの価値が高いコード
      • 色々なドメインクラスやビジネスロジックを繋ぐ(連携を指揮する)
      • いわゆるコントローラ
    • テストの価値がないコード
      • コンストラクタとか
  • ではどんな具合に「テストしたいが困難なコード」をビジネスロジックとコントローラに分解していくか
    • 策1:分離を突き詰める(ビジネスロジックとコントローラ両方を清潔にする)
      • もしかして:関数型アーキテクチャ(第6章参照)
      • -> パフォーマンスが犠牲になる
        • パフォーマンスは大抵捨てがたいので、この策は選べない
    • 策2:パフォーマンスを守りつつ、コントローラを清潔に保つ
      • すなわち、ドメインモデルにプロセス外依存を注入する
      • -> 単体テストの記述が難しくなる
        • この策も避けたい
    • 策3:パフォーマンスを守りつつ、ビジネスロジックを清潔に保つ
      • すなわち、決定を下す過程をさらに細かく分割する
      • -> コントローラの簡潔さを諦めることになる
        • これが一番妥当ではないでしょうか
  • コントローラの複雑化に抗う手法
    • 確認後実行(CanExecute/Execute)パターン
      • コントローラはCanExecute, Executeの順でメソッドを呼び出すだけでいい、という手法
      • Execute内でもCanExecuteを呼び出すようにしておくことで、万一コントローラの実装者がCanExecuteを呼び忘れても破壊的処理が行われないようにする
    • ドメインイベント
      • ドメインから外に処理を行使したいときに、代わりにドメイン内の「こんなことが起きたよ」的リストにイベント情報を詰めるだけにしておく
      • そしてコントローラは、ドメインロジック実行後にそのリストを見てイベントを具現化する、という手法

Discussion