t0mmy

2020年12月30日に参加

学習履歴詳細

単体テストの考え方/使い方 8章 読了

やったこと

  • 単体テストの考え方/使い方 8章 読了

学んだこと

ポイント

  • 統合テストの役割
  • テスト・ピラミッドについての更なる考察
  • 価値のある統合テストの書き方

学び

単体テスト 統合テスト
検証対象 ドメインモデル ドメインモデルとプロセス外依存を結びつけるコード
ケース 出来るだけ多くの異常系を検証 最長のハッピーパス一件のと、単体テストでは検証できない異常系全て
  • 単体テスト/統合テストの棲み分け
  • テストピラミッドは、プロジェクトの複雑さによって形が変わり得る
  • 価値の低い(または無い)テストケースは、作成しない方が良い
  • 具象クラスがひとつしかないI/Fは、抽象化の恩恵が少ないだけでなく、コードの視認性が悪化する問題を抱える
  • 管理下にない依存をモック化したい場合は、具象クラスが1つであってもI/Fが有効
    • 管理下にある依存は、具象クラスとして実装し、コントローラに注入する
  • すべての依存(ログ出力オブジェクトも含む)は、常に、コンストラクタやメソッドの引数を経由して、明示的に注入されるようにすべきである

気づき

  • 手元の動作確認には 単体テストを、CI/CDテストでは 単体テスト & 統合テスト を、リリース直前ではE2Eテストを実行する ... という棲み分けか

メモ

単体テストだけではなく、外部依存とのやり取りを含めた検証も必要。
このような検証が、統合テストに当たる。

8.1 統合テストとは?

単体テストが持つ、以下の性質を一つでも損なっているものが、統合テストに分類される。
- 一単位の振る舞いを検証する事
- 実行時間が短い事
- 他のテスト・ケースから隔離された状態で実行されること

具体的には、プロセス外依存を交えた動作を検証することが多い。
- これは、コントローラをテストすることを意味する
プロセス外依存を全部 Mock に置き換えたコントローラのテストは、単体テストに分類できる。

まとめると、...
- 単体テストでドメインモデルを検証
- 統合テストで「ドメインモデルとプロセス外依存を結びつけるコード」を検証

単体テストと統合テストのバランスを保つ

単体テストと統合テストは、それぞれ利点/欠点がある。
- 単体テストは、保守性が高く、リファクタリング容易で、すぐに動作完了するが、プロセス外依存を含めた動作は確認できない
- 統合テストは、プロセス外依存を含めた動作を検証できるが、遅く、保守コストが高くなる

これらを踏まえ、いかに必要最低限のテストケースで済ませることが出来るかを考えることになる。

  • 単体テストでは、多くの異常系を検証する
  • 統合テストでは、一件のハッピーパスと、単体テストでは検証できないすべての異常系を検証する

以上より、以下の指針が立つ。
* 出来るだけ多くのケースを単体テストで作成し、保守コストを下げる
* ビジネス・シナリオ一つにつき、1,2件ほど統合テストを実施し、システムの機能に自信を持つ

設計原則

早期失敗

望まないエラーを検知した場合、すぐにその処理を停止させる、という原則。
以下の利点を享受でき、アプリケーションが堅牢になる。
- フィードバック・ループの短縮 : より速やかにバグを検出でき、修正につなげることが出来る
- 保存される状態の保護 : 壊れたデータが、データストアに保存されにくくなる

-> 例外処理や、事前条件の確認などが、早期失敗の原則に則った設計に該当

8.2 どのようなプロセス外依存をモックに置き換えるべきか?

プロセス外依存の種類

  • 管理下にある依存 : テスト対象のアプリケーションが、完全に制御可能な依存のこと
    • ある処理専用のDB(テーブル)など
    • -> 「実装の詳細」に該当する
  • 管理下にない依存 : テスト対象以外のアプリケーションも、動作を制御可能な依存のこと
    • 複数のビジネスロジックで共有するDB(テーブル)、メールなど
    • -> 「観察可能な振る舞い」に該当する

プロセス外依存をモックに置き換える時の指針

  • 管理下にある依存は、「実装の詳細」のため、そのまま使用する
    • 言い換えると、モックに置き換えたりしない
  • 管理下にない依存は、「観察可能なふるまい」のため、モックに置き換える

統合テストにて、「管理下にある依存」のDBを使えない場合は、統合テストを作成しない。
- モックなどで無理矢理テストを書いても、コスパが悪いだけ

8.4 インターフェースを使った依存の抽象化

単体テストの文脈において、誤ったインターフェースの使用が目立つ。
これにより、インターフェースの過剰使用が多々発生している。
インターフェースの誤った認識を確認し、どのような場合にインターフェースを用いるべきか議論する。

I/Fの間違った認識

  • 誤った認識 : I/F により、プロセス外依存を抽象化でき、疎結合化できる
    • I/Fの実装が一つしかない場合、そのI/Fは抽象ではなく、疎結合化に寄与しない
  • 既存のコードを変更することなく機能追加が可能になり、Open/Closed 原則を順守しやすくなる
    • Open/Closed 原則以前に、 YAGNI原則に外れる
    • Open/Closed 原則順守のために、不要なコードの作成にコストをかけるのは本末転倒

「一つのI/Fに対して一つの実装」が有効な場面

「管理下にない依存」のモックを作成する必要がある場合、I/Fの実装が有効。
* 「管理下にある依存」は具象クラス化し、コントローラに注入する形をとる
* 単体テストではモッククラスを、統合テストではプロダクションコードで使用する具象クラスを注入し、動作確認を行う

ドメインロジックで使用するようなクラスのI/Fが存在する(当然、一クラス一I/F)場合、問題を疑うこと。
* ドメインロジックの動作(≒実装の詳細)を検証しようとしている可能性が高い
* 実装の詳細を検証するテストは壊れやすく、テストのリファクタリング体制を低下させる

8.5 統合テストのベストプラクティス

以下のベストプラクティスが存在する。

  • ドメインモデルの境界を明確にする
  • アプリケーションを構成する層を減らす
  • 循環依存を取り除く

ドメインモデルの境界を明確にする

ドメインモデルの境界を明確にする(≒ドメインモデルに関するコードを見つけやすくする)ことで、以下のメリットを享受できる。
- 単体テストで検証すべきコードが明確になる
- ドメインモデルの検証 = 単体テスト
- アプリケーションが解決したい問題が明確になる
- ドメインモデルは、解決したい問題のドメイン知識を集めたもの

アプリケーションを構成する層を減らす

コードの抽象化や汎用化を行う際、間接参照の層を設けることは少なくない。
間接参照の層が厚くなるほど、コードの可読性が低下する。
そのため、間接参照の層は、可能な限り少なくなるよう努めよう。
- 多くの場合は、「アプリケーションサービス層(コントローラ)」「ドメイン層」「インフラ層」で十分
- 「インフラ層」では、プロセス外依存に関する操作を扱う

循環依存を取り除く

循環依存を持つコードには、以下の問題がある
- 可読性が低下する
- 単体テストの際、一つの振る舞いを切り取ることが困難になる
特に、コールバックで発生しやすい。

循環依存の原因がコールバック関数の場合、コールバック関数を呼び出すのではなく、コールバック関数が期待する値オブジェクトを定義することで解決できる。

before

const callback = (result) => {...}
const sample = (callback) => {...} // 関数callbackは、result 変数を期待する

after

const sample = () => { return ...}
const result = sample() // result 変数を受け取る
callback(result) // これで、sample() に callback を渡す必要がなくなった

1つのテストケースに、複数の実行フェーズを用いる場合

基本的には、一つのテストケースにつき、実行フェーズは一つ。
- 複数ある場合は、設計やテストケースの問題である可能性が高い
- 設計の問題 : テスト対象のメソッド一つが、複数の責務を負っている可能性
- テストケースの問題 : 一つのテストケースで、複数の振る舞いを検証しようとしている

例外的に、「開発者が、プロセス外依存を制御できない場合」は、実行フェーズを複数設ける。
- 言い換えると、制御可能なプロセス外依存は「制御可能」であるため、テストケースを複数に分割できるはず

他には、E2Eテストも、複数の実行フェーズを定義する。

8.6 ログ出力に対するテスト

ログ出力をテストするべきか、判断する指針

  • サポートログか、診断ログか
    • サポートログ : 開発者以外(運用者、クライアント、ユーザなど)が見ることを意図した、特定のイベントを記録するログ
    • 診断ログ : 開発者のみが見る、開発中のデバッグログ
  • ログ出力がユーザの要望か否か(≒ビジネス要求か否か)

ログ出力のテスト方法

サポートログの場合は、テストするべき。
テストの際は、DomainLogger のような、ビジネス要求を反映したLogger クラスの定義する。
そして、上記クラスに、記録したいイベントのログを吐き出す専用メソッドを定義する。

イメージ

domainLogger.UserTypeHasChanged(args...)

これにより、どのようなサポートログを出すべきかが、コードから読み取れるようになる。
これは、保守が容易になることを意味する。

DomainLogger のようなクラスは、ドメインロジックに直接記入しない。
ドメインロジック側でドメインイベントを返却するようにし、返却されたドメインイベントを基に、DomainLoggerのようなクラスを用いてログを吐き出す。
各種テストでは、以下を確認する。
- 単体テストの際は、目的のドメインイベントが生成されたか確認する
- 統合テストでは、モックを用いて、想定したログ出力メソッドが呼ばれたか確認する

ログ出力オブジェクトは、コンストラクタやメソッドの引数に渡す。


まとめ

  • 統合テストは、テスト対象がプロセス外依存と統合した状態で行うテスト
    • 統合テストでは、コントローラを検証する(一方で、単体テストはドメインモデルを検証する)
    • 統合テストは、退行保護(バグ検出)とリファクタリングに優れる
    • 一方で、単体テストは、保守容易性と、フィードバックの速さに優れる
  • 統合テストでは、単体テストでは備えることのできない、退行保護とリファクタリングに重きを置く
    • 必要なら、フィードバック速度や保守容易性は犠牲にする
    • この辺りを図示したものがテストピラミッド
  • テストピラミッドの形状は、プロジェクトの複雑さによって変化する
  • 早期失敗(Fail Fast):「問題(バグ)が発生したら、すぐに処理を失敗させる」という原則
  • 管理下にある依存(managed dependency) : テスト対象のアプリケーションを介してのみアクセス可能なプロセス外依存のこと
  • 管理下にない依存(unmanaged depencency) : テスト対象以外からもアクセス可能なプロセス外依存のこと
  • 統合テストでは、管理下にある依存には実際の依存を使用し、管理下にない依存にはモックを使用する
  • 統合テストでは、「管理下にある依存」を扱う層をすべて経由kること
  • 実装クラスを一つしか持たないようなI/Fは抽象ではない
    • 「将来に備えて」設けられたこのようなI/Fは、TAGNI原則から外れる
    • 一方で、テストの際にこのI/Fをモックに置き換えたいような場合は、効果あり
      • ただし、基本は「管理下にない依存」に対するI/Fのみ
    • もし、このようなI/Fが同一プロセス内の依存に使用されているのならば、設計を疑うこと
      • モックを使ってドメインクラス間でのやり取りを検証しようとしている
      • これは、実装の詳細をテストしようとしていることを意味する
  • ドメインモデルは、コードベースから見つけやすいところに配置しよう
    • ドメインクラスとコントローラの境界が明確になっていれば、単体テストと統合テストを分離しやすくなる
  • 間接参照の層が増えると、コードが分かりにくくなるため、できるだけ少なくしよう
    • バックエンドであれば、ドメイン層、アプリケーションサービス層、インフラ層で十分
  • 循環依存は、コードが分かりにくくなる
    • コールバック処理で発生しやすい
    • コールバックの場合は、コールバックではなく、処理結果を詰めた値オブジェクトを返すことで、循環依存を解消できる
  • 統合テストでは、以下のような場合に限り、Act フェーズが複数存在できる
    • プロセス外依存に何らかの制約がある場合
  • システム運用者が見ることを意図したログ(サポートログ)は、観察可能な振る舞い
    • 一方で、開発者が見ることを想定したログは、実装の詳細に分類される
  • サポートログは、ビジネス要求であり、DomainLogger クラスのような実装が必要
  • すべての依存は、常に、コンストラクタやメソッドの引数を経由して明示的に注入されるようにすべきである
テスト

2023年05月01日(月)

3.0時間