t0mmy

2020年12月30日に参加

学習履歴詳細

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

やったこと

  • 単体テストの考え方/使い方 7章 単体テストの価値を高めるリファクタリング 読了

学んだこと

この章で取り扱う事

  • プロダクション・コードの種類の識別
  • 質素なオブジェクト
  • 価値あるテストケースを作成する方法

ポイント

  • 質素なオブジェクト
  • コードの深さ、コードの広さ
    • または「ドメインにおける重要性」と「協力者オブジェクトの数」
  • 「ロジックに関する責務」と「コード間の連携に関する責務」を分離する
  • 副作用は、ビジネスロジックで実行させない
    • そのために、副作用を伴う処理を、可能な限り遅延させる
  • 抽象化する対象をテストするよりも、抽象化された結果をテストする方が簡単である

学び

  • ヘキサゴナルアーキテクチャ、関数型アーキテクチャは、質素なオブジェクトパターンを適用したアーキテクチャ
  • 最小限の保守コストで最大限の価値を生み出すテストを作成するために、以下を実施できること
    • 価値あるテストケースを識別できる
      • 言い換えると、価値の少ない(または無い)テストケースを認識できる
    • 価値あるテストケースを作成できる
  • まずは、プロダクションコードから、リファクタリングが必要な部分を識別できること
    • 「コードの複雑さ」と「協力者オブジェクトの数」の視点で識別できる
  • Active Record パターン
    • ドメイン・クラスが、DBから自身のデータを取得・保存できる設計パターン
  • ビジネスロジックに副作用を持ち込みたくない
    • 副作用を伴う処理とビジネスロジックを分離し、副作用を伴う処理を可能な限り遅延することで実現できる

気付き

  • コントローラの責務
    • View と Model 、 Model と Model間など、二つ以上のコンポーネントを取り持つ(言い換えると、両者の処理の流れを制御する)ことに責務を持つ
  • ドメインロジックと外部依存を分離することが大事
    • 分離の際は、両者の橋渡しをするコントローラが必要になる
    • 最終的には、「ビジネスロジック」「外部依存」「コントローラ」という分離になるか
  • 役割
    • 「ビジネスロジック」: 引数で受け取ったデータを基に、決定を下すだけ
    • 「外部依存」: 外部APIを叩いたり、DBへアクセスしたり
    • 「コントローラ」: ビジネスロジックと外部依存を橋渡しする
  • 「ビジネスロジック」は、単体テストで集中的に
  • 「コントローラ」は、統合テストに属する
    • この時、「外部依存」は 全部 Mock となる予感

メモ

リファクタリングが必要なコードの識別

「コードの複雑さ」と「協力者オブジェクトの数」の視点で分類できる。

![[Pasted image 20230422130837.png]]
* 協力者オブジェクトの例) 非同期、マルチスレッド処理、UI、プロセス外依存とのコミュニケーションなど

一般的に、以下が知られている。
* 協力者オブジェクトが多いほど、単体テストの保守コストが増大する
* コードの複雑さが多いほど、単体テストの価値が高くなる

以上より、以下のことが言える。

  • 「ドメイン・モデル アルゴリズム」の単体テストに時間をかける
  • 「コントローラ」の単体テストに時間をかけない
    • 「コントローラ」のテストは、統合テストで実施する
  • 「取るに足らないコード」のテストに時間をかけない
  • 過度に複雑なコードは、リファクタリングによって「ドメイン・モデル アルゴリズム」と「コントローラ」に分割する

「取るに足らないコード」や、質の悪いテストは、リファクタリングの過程で取り除くことになる。

質素なオブジェクトを用いた、過度に複雑なコードの分割

過度に複雑なコードは、「ドメイン・モデル アルゴリズム」と、協力者オブジェクト(簡単にはテストできない依存)から成る。

過度に複雑なコードを、以下の二つに分離する。
* 「ドメイン・モデル アルゴリズム」に相当するコード
* テストが困難な依存で構成される質素なクラス
「ドメイン・モデル アルゴリズム」を、作成した質素なクラスで包みこむことで、プロダクションコードを動作させる。
* この時、質素なクラスには、ロジックをほぼ(または全く)持たせないこと。

このように、過度に複雑なコードを、ロジックに関する責務と質素なクラス(コントローラの責務)に分離するパターンを、質素なオブジェクトパターンと呼ぶ。

このようにして責務を分離すると、テストが容易になるだけでなく、プロダクションコードの保守性も向上する。

関数型アーキテクチャとヘキサゴナルアーキテクチャは、質素なオブジェクトパターンを適用したアーキテクチャ。

「コードの深さ」と「コードの広さ」

[[#質素なオブジェクトを用いた、過度に複雑なコードの分割]]で述べたように、「ロジックに関するコード」(ドメイン・モデル アルゴリズム)と「コード間の連携を指揮するコード」(コントローラ)の責務分離は、非常に重要。

この責務分離について、書籍では、以下のように定義している。
* ロジックに関する責務 :「コードの深さ」 (≒複雑さや重要性)
* コード間連携の指揮に関する責務 :「コードの広さ」(協力者オブジェクトの数)

事前条件はテストするべきか

明確なルールは存在しない。

ドメインにとって重要な事前条件であれば、十分テストする価値はある。

コントローラにおける条件付きロジックの扱い

決定を下す過程で、ある処理で得た結果を基にプロセス外依存から新たにデータを取得するような場合を考える。
* この時、コントローラに、「プロセス外依存とのやり取りを決定する」ロジックを記述することになる

この解決策は、以下の3つ。

  1. 外部依存に対する読み書きを、処理の開始/終了 部分に全て持っていく
  2. ドメイン・モデルに、プロセス外依存を注入する
  3. 決定を下す過程をさらに細かく分割する

上記の内、どの解決策を選択するべきかを判断したい。
この場合、以下に挙げる要素のバランスが取れているかを考えると良い。

  • ドメイン・モデルのテストしやすさ
  • コントローラの簡潔さ
  • パフォーマンスの高さ

上記要素は、二つまでしか備えることが出来ない。

各解決策に対する、書籍での見解

  • 外部依存に対する読み書きを、処理の開始/終了 部分に全て持っていく
    • 非推奨 : パフォーマンスが犠牲になるため非推奨
  • ドメイン・モデルに、プロセス外依存を注入する
    • 非推奨 :ビジネス・ロジックをプロセス外依存とのコミュニケーションから分離できなくなる
  • 決定を下す過程をさらに細かく分割する
    • 推奨 : コントローラが複雑になるが、ある程度軽減可能
    • そもそも、コントローラから全ての複雑さを取り除くことは不可能

「決定を下す過程を更に細かく分割する」戦略をとる場合、[[#CanExecute/Execute パターン]]
や[[#ドメインイベントの利用]]を使用することで、コントローラの複雑さをある程度軽減できる。

これらを使用すると、決定を下す過程を、ドメインモデルに移管できる。

CanExecute/Execute パターン

あるコードを、「処理Aを行うか判断するコード」(CanExecute)と、「処理Aを行うコード」(Execute)の二つに分離するパターン。

  • CanExecute はビジネス・ロジックに相当

これにより、ビジネス・ロジック(≒CanExecute部分)が、コントローラへ流出することを防ぐ。

ドメインイベントの利用

ドメインにとって重要なイベントが発生したことを、プロセス外依存に伝えたい場合がある。
「プロセス外依存にイベントを伝えるべきか」の判断は、ドメインモデルに判断させたい。

このような場合、以下の手法で解決できる。
* 発生したドメインに関するイベントを、インスタンスで表現する
* インスタンスの有無で、プロセス外依存にイベントを伝えるかどうかを判断する

「ドメインにとって重要なイベント」を表現したインスタンスを、ドメインイベントと呼ぶ。

ドメインイベントは、「ドメインエキスパートにとって意味のあるイベント」を指す。
イベント駆動(クリックイベントなど)の文脈で用いるイベントとは異なる(こちらは、いうなら技術的なイベント)。

ドメインイベントを導入することで、「プロセス外依存へイベントを伝えるか」を、ドメインモデルが判断できるようになる。
* ドメインイベントが存在しない場合、この判断はコントローラで実装することになりかねない

ドメイン層から副作用を取り除く まとめ

抽象化したいこと 抽象化する手法
「外部依存へメッセージを送信する」処理 送信したいイベントを、ドメインイベントで管理する
DBへの変更処理 ドメイン・クラスの状態を変更する

副作用は、ビジネスロジックで実行させない。
そのために、副作用が発生する処理は、ビジネスオペレーションの最後まで実行させないようにする。
* 言い換えると、副作用を伴う処理の実行を遅延させる
そのための手段の一つが、「ドメインイベントの利用」、「CanExecure/Execute パターン」、「ドメイン・クラスによる状態の管理」。

そして、抽象化する対象をテストするよりも、抽象化された結果をテストする方が簡単である。


まとめ

  • コードの複雑さは、コードにおける「決定を下す箇所(分岐)」の数によって判断できる
  • 「決定を下す箇所」は、コードに記述してあることもあれば、コードが使用しているライブラリ内に存在することもある
  • ドメインにおける重要性は、対象となるコードがドメインの問題領域においてどれだけ重要かを示すもの
  • 複雑なコード、および、ドメインにおける重要性が高いコードは、単体テストを行う価値が高い
  • 単体テストにおいて、協力者オブジェクトの多さは、テストの保守困難性の高さに直結する
    • 「テストに必要な協力者オブジェクト」を準備するコードや、テスト後に協力者オブジェクトの状態を検証するコードが増えるため
  • 全てのプロダクションコードは、コードの複雑さやドメインにおける重要性の観点、および協力者オブジェクトの数のか観点から、以下の4つに分類できる
    • ドメイン・モデル/アルゴリズム
    • ドメインにおける重要性が高く、協力者オブジェクトが少ない
    • 単体テストの費用対効果が最も高い
    • 取るに足らないコード
    • ドメインにおける重要性が低く、協力者オブジェクトが少ない
    • テストする価値が少ない(または無い)
    • コントローラ
    • ドメインにおける重要性が低く、協力者オブジェクトが多い
    • 統合テストでテストするべきコード
    • 過度に複雑なコード
    • ドメインにおける重要性が高く、協力者オブジェクトが多い
    • ドメイン・モデル/アルゴリズムとコントローラに分離するべき
  • コードの複雑さや重要さが増すにつれて、協力者オブジェクトの数を減らすべき
  • 質素なオブジェクト は、過度に複雑なコードからビジネスロジックを別のクラスに抽出し、テストを行いやすくする設計パターン
    • ヘキサゴナルアーキテクチャ、関数型アーキテクチャは、実は質素なオブジェクトに該当する
  • ビジネスロジックに関する責務は、コードの深さ(複雑さや重要性)としてみることが出来る
  • 連携の指揮に関する責務は、コードの広さ(協力者オブジェクトの多さ)としてみることが出来る
  • コードは、コードの深さと広さの両方を持ってはならない
    • 言い換えると、コードの深さと広さの、どちらか一方に寄せるべき
  • 「事前条件」は、ドメインにとって重要である場合に限ってテストする
    • 費用対効果の問題
  • ビジネスロジックのコードと指揮するコードを分離する場合、バランスを考えなくてはならない3つの重要な性質がある
    • ドメインモデルのテストのしやすさ : 協力者オブジェクトの数と種類に影響される
    • コントローラの簡潔さ : コントローラの決定を下す箇所(分岐)の数に影響される
    • パフォーマンスの高さ : プロセス外依存への呼び出しを行う回数に影響される
  • 上記性質は、2つまでしか備えることが出来ない
  • 決定を下す過程を更に細かく分離することは、上記性質の長所と短所を考慮した、最善のトレードオフ
  • 次の二つの設計パターンを用いることで、コントローラの複雑さを緩和できる
    • 確認後実行パターン
    • ある処理について、「処理を行ってよいか確認するメソッド」と「処理を実行するメソッド」に分離する
    • 「処理を実行するメソッド」を実行する際、必ず事前条件を満たしていること(≒処理を行ってよいか)を保障できる
    • => コントローラから、決定を下すか確認する責務を切り離すことが出来る
    • ドメインイベント
    • ドメイン・モデルで発生する重要な状態の変更を追跡できる
    • 発生したドメイン・イベントを基に、プロセス外依存を呼び出すようにする
    • => コントローラから、状態変更を追跡する責務を切り離すことが出来る
  • 抽象化する対象をテストするより、抽象化された結果をテストする方が簡単である
    • ドメイン・イベントは、プロセス外依存への呼び出しに対する抽象化
    • ドメインクラスに対する状態の変更は、データ・ストレージへの状態の変更に対する抽象化
テスト

2023年04月22日(土)

3.5時間