logo

2023219

ビジネス・ロジックと連携の指揮を分離すれば良いテストが書ける:単体テストの考え方/使い方 第2部後半

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)を読んでいるので、そのまとめを部ごとに書いていこうと思います。

  1. 単体テストの目的・定義・学派・命名について:単体テストの考え方/使い方 第1部
  2. リファクタリングしやすいテストを書こう:単体テストの考え方/使い方 第2部前半
  3. ビジネス・ロジックと連携の指揮を分離すれば良いテストが書ける:単体テストの考え方/使い方 第2部後半(この記事)
  4. プロセス外依存は統合テストで確認しよう:単体テストの考え方/使い方 第3部

今回は第2部「単体テストとその価値」の後半についての感想と考察になります。第2部は以下の4章で構成されていますが、今回は後半の第6章と第7章について扱います。

  • 第6章:単体テストの3つの手法
  • 第7章:単体テストの価値を高めるリファクタリング

関数型プログラミング

単体テストの手法には以下の3つが存在します。

  • 出力値ベース・テスト(戻り値を確認するテスト)
  • 状態ベース・テスト(状態を確認するテスト)
  • コミュニケーション・ベース・テスト(オブジェクト間のやり取りを確認するテスト)

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)p.167 より引用し箇条書きにまとめた。

著者は、リファクタリングへの耐性と保守のしやすさを維持するのに必要なコストの観点から「出力値ベース・テストがもっとも費用対効果の高いテスト・ケースを作成できる」としています。そして、出力値ベース・テストは、「関数型による単体テストの手法」であることから、第6章では主に関数型プログラミング・関数型アーキテクチャについて解説されています。

  • 関数型プログラミングとは、数学的関数を用いたプログラミングのことです。そして、この数学的関数(mathematical function)は純粋関数(pure function)とも呼ばれ、隠れた入力や出力がない関数(もしくはメソッド)のことになります
  • 関数型アーキテクチャとは何か?
    • 関数型アーキテクチャでは、純粋関数(不変)を用いて書かれたコードを最大限に増やす一方、副作用を扱うコードを最小限に抑えるようにします
    • 関数型アーキテクチャでは、副作用をビジネス・オペレーションの最初や最後に持っていくことで、ビジネス・ロジックと副作用とを分離しやすくしています
    • 決定を下すコードは関数的核(functional core)もしくは不変核(immutable core)と呼ばれるのに対し、決定に基づくアクションを実行するコードは可変殻(mutable shell)と呼ばれます

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)pp.181-185 より引用し箇条書きにまとめた。太字は筆者によるもの。

私は関数型プログラミング・関数型アーキテクチャについて何も知識がなかったのですが、本書の解説によってそのあらましを理解することができました。そして、関数型アーキテクチャの話を前提に第7章へと続いていきます。

ビジネス・ロジックと連携の指揮を分離させよ

MVC(Model-View-Controller)の考え方でも重要な、ビジネス・ロジックとコントローラの分離ですが、これを適切に行うことで、「特に重要な部分だけがテスト対象」となっていて、「最小限の保守コストで最大限の価値を生み出す」優れたテスト・スイートが書けるようになります。

著者はプロダクション・コードを次の4種類に分類しました。

  • ドメイン・モデル/アルゴリズム:コードの複雑さ/ドメインにおける重要性が高いが、協力者オブジェクトの数は少ない
  • 取るに足らないコード:コードの複雑さ/ドメインにおける重要性が低く、協力者オブジェクトの数も少ない
  • コントローラ:協力者オブジェクトの数は多いが、コードの複雑さ/ドメインにおける重要性は低い
  • 過度に複雑なコード:協力者オブジェクトの数が多く、コードの複雑さ/ドメインにおける重要性も高い

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)p.217 より引用し箇条書きにまとめた。太字は筆者によるもの。

この「過度に複雑なコード」がどげんとせんといかんヤツで、「ドメイン・モデル/アルゴリズム」と「コントローラ」に分割していきます。そのために導入されるのが「質素なオブジェクト(Humble Object)」という設計パターンです。

  • 質素なオブジェクト自体にはロジックを(ほぼ)含ませないようにすることで、質素なオブジェクトをテストする必要がないようにする
  • このパターンは各クラスが持てる責任を1つだけにする単一責任の原則(Single Responsibility Principle: SRP)を遵守するための手段としても見ることができます
  • MVP(Model-View-Presenter)パターンやMVC(Model-View-Controller)パターン……では、PresenterもしくはControllerが質素なオブジェクトとなり、ビューとモデルを結びつける役割を担います

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)pp.220-222 より引用し箇条書きにまとめた。

本書では上記を実現するためのサンプル・コードのリファクタリングが分かりやすく解説されており、とても参考になりました。個人的にはコントローラという言葉の意味をちゃんと理解せずにプログラムを書いてしまっていた(ほかの人のアーキテクチャの猿真似をしていた)ところがあったので反省しました……。

そのため、コントローラと認識しつつ、普通に分岐が入ってしまっていて過度に複雑なコードになってしまってたのですが、本書では「コントローラにおける条件付きロジックの扱い」についても言及されています。

ビジネス・ロジックのコードと連携を指揮するコードの分離をもっとも行いやすいのは、1つのビジネス・オペレーションが次の3段階の流れになっている場合です:

  1. ストレージからのデータの取得
  2. ビジネス・ロジックの実行
  3. 変更されたデータの保存

しかしながら、これらの手順を完璧に順守できない場合もよくあります。たとえば……決定を下す過程の中で、途中で得た結果を使ってプロセス外依存から新たにデータを取得しなければならないような場合です。

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)p.240 より引用。一部省略。

そして、この課題を解決するための選択肢としては以下の3つがあります。

  • 外部の依存に対するすべての読み込みと書き込みをビジネス・オペレーションの始めや終わりに持っていく
    • パフォーマンスは劣化する
    • パフォーマンスの高さは重要な性質である → 選べない
  • ドメイン・モデルにプロセス外依存を注入する
    • ドメイン・モデルに対するテストのしやすさは失われる
    • ほとんどのコードを過度に複雑なコードに属するようにしてしまいます → 選べない
  • 決定を下す過程をさらに細かく分割する
    • コントローラの簡潔さを保てなくなる
    • ある程度であれば対処することが可能 → 選べる

『単体テストの考え方/使い方』(Vladimir Khorikov 著、須田智之訳)p.241 より引用し箇条書きにまとめた。太字は筆者によるもの。

前の2つの選択肢については容認できない欠点があるので選ぶことはできないのですが、3つ目の選択肢については許容できるので、本書では「決定を下す過程をさらに細かく分割する」ことが推奨されています。

「決定を下す過程をさらに細かく分割する」ための方法として「確認後実行(CanExecute/Execute)パターン」があります。

ドメイン・モデルに CanChangeEmail のような確認するためのメソッドを追加し、これを ChangeEmail メソッドをたたくための事前条件とすることで、コントローラは事前条件の中身については知らずに済むので、ビジネス・ロジックがコントローラに流出するのを防ぐことができるようになります(p.243)。

これは非常に有用なテクニックだと思いました。
コントローラ(私の参加するチームでは MVC を使っていないのでそれっぽいもの)に、ビジネス的な条件分岐を書いていて、どこまでがドメイン知識でどこからがそうでないのかが曖昧でモヤモヤすることもあったので、今後はこのようなテクニックを活用して、ビジネス・ロジックとコントローラとの分離を明確にしていきたいと思います。

観測可能なふるまいの範囲

前回の記事で気になっていた「外部から観測できないプロセス外依存とのコミュニケーションを実装の詳細にしちゃったら、それほぼ E2E テストだから迅速なフィードバック得られなくない?」に対する著者の答えは「玉ねぎの層のように考える」ことでした。

外部クライアントにとっての観測可能な振る舞い、コントローラにとっての観測可能な振る舞い、そしてコントローラで呼び出されるメソッドにとっての観測可能な振る舞いはそれぞれ異なっていて、それぞれにとって直接関係ないことについては意識しない、ということのようです。前章では究極を言えば外部クライアントからのテストだけで良いのではという話になっていましたが、それは違う、ということですね。

それはそう、という感じはありますが、実際にそのようにテストを作るには、今回の範囲で取り上げたビジネス・ロジックとコントローラの分離が適切に行われていないと、否が応でも直接関係しない呼び出しについても意識せざるを得なくなる気がします。例えば、呼び出し先でさらに DB に依存するクラスを呼び出している場合には DB のモックを作る必要が出てきます。

ビジネス・ロジック(関数的核)のクラスから、モックを必要としてしまうプロセス外依存を徹底的に排除することで(これが関数型アーキテクチャの思想)、きれいにテストを書くことができるようになります。回りまわって関数型アーキテクチャの解説がされていたことに腑に落ちました。

まとめ

以上、第2部「単体テストとその価値」の後半についてのまとめと所感でした。アーキテクチャについては理解していない部分が多く、参考にしているほかのプロジェクトのリポジトリを見ながら唸っていたりするので、今回の範囲ではそうしたところが次々と分かるようになりました。良い単体テストを書くことを目指すと、プロダクション・コードも改善されていくのだなと感じました。