読む前。本の存在を知り、感想を知り、自分の学びたいことを記す
- 自動テスト全体の信頼性を維持するためにはどうするか 「ブレない基準でピラミッドを作り、スモールに切り出していく」 – ログミーTech https://logmi.jp/tech/articles/329185
最近良い本が出過ぎて、私の仕事を脅かすようなライバルが出てきています。例えば『単体テストの考え方/使い方』という本があります。この本は内容が良過ぎて、私がいろいろ説明したいことがほとんどこの本に書いてあるような事態になっています。とても良い本なのでおすすめです。私の活躍できる領域を明らかに蝕んできているぐらい、良い本です。
- 動画はこれ。
- の次の部分
これでこの本を認識し、興味を持った。
「コードレビューするときに使えるフレーズを探す」と念頭におけば読む時のとっかかりになって捗りそう。他に→「自分が普段書いているテストコードは本の中ではどの位置にいるか?」
どういうパターンのテストを書け、質の良いテスト・ケースを作る方法、は取り扱ってる?境界値とか。
読む前や読んでいる途中に残した、本の内容以外についてのメモ
- この本について
- 単体テストの考え方/使い方 | マイナビブックス https://book.mynavi.jp/ec/products/detail/id=134252
- 単体テストの考え方/使い方 https://amzn.to/4cOZcHA
- 『単体テストの考え方/使い方』|感想・レビュー・試し読み – 読書メーター https://bookmeter.com/books/20438696
- 動画の中で紹介されていた、別の書籍、気になった。テストの大きさの考え方、より曖昧さの少ない分類「テストサイズ」
- 自動テストの種類の曖昧さが少ない「テストサイズ」という分類 スコープとの掛け合わせでわかる“コスパの良いテスト” – ログミーTech https://logmi.jp/tech/articles/329184
- Googleのソフトウェアエンジニアリング ―持続可能なプログラミングを支える技術、文化、プロセス https://amzn.to/3LvGrga
- 読んだ人の感想やまとめ
- 単体テストの考え方/使い方のメモ・単体テストについて学んだこと|くぼぴー https://note.com/kubopi/n/n6715a813ef1b
- 単体テストの考え方/使い方 まとめ https://zenn.dev/yudai64/articles/c1f7fba3c93536
- 単体テストの考え方/使い方を読んだ。読んでよかった。 – Mitsuyuki.Shiiba https://bufferings.hatenablog.com/entry/2024/08/02/010813
- 読書中だが、本に書かれている内容を身につけたらこうなる、という感じの参考ページ
- 【t-wada】自動テストの「嘘」をなくし、望ましい比率に近づける方法【Developer eXperience Day 2024 レポート】 | レバテックラボ(レバテックLAB) https://levtech.jp/media/article/column/detail_496/
- テストを書く方針と原則の備忘録 #テスト自動化 – Qiita https://qiita.com/nsym__m/items/a796d16c3999a1801dba
- (5) 開発生産性の観点から考える自動テスト / タワーズ・クエスト株式会社 和田 卓人 – YouTube https://youtu.be/ueqjypYJnxk?si=QzqvZPH53DbOQThm
- (5) 【DXD2024】望ましい自動テストとは: どのようなテストが開発生産性と開発者体験を共に高めるのか(7/16 16:00〜16:45) – YouTube https://youtu.be/8MiWDWAG1XQ?si=iE0439H0NG8vDuuB
本を手に取ってみて
分厚くて、文字も小さい(本書の直前に読んだ本よりも)。399ページある。
感想
第1部: 単体(unit)テストとは?
第1章 なぜ、単体(unit)テストを行うのか?
第2章 単体テストとは何か
実際自分が書いているテストコードには、古典学派とロンドン学派の中間位のところになると思う。では何が境目になるのか。例えば外部への問い合わせ、レストフルAPIとかやるところが基準になるのではないか。そしてそういった境目はログ出力をするための基準にもなるのではないか。
以上のように少し読んで予想した。
自分が普段書いているのは、本書の定義ではおそらくユニットテストには当てはまらなかった。統合テストに当てはまった。自分の書いている統合テストは「1単位の振る舞いを検証すること」を強く意識したもので、「他のテスト・ケースから隔離された状態で実行されること」も満たすように書いている。自分の書くテストコードはプロセス外依存、データベース、を基本的に孕んでいる。これにより「実行時間が短いこと」を満たしにくいが、インメモリのSQLite を使うことで実行時間を短くする努力をしている。ただし、1単位の振る舞いをテストするための前提が複雑になり事前準備のデータベースレコードが増えることで実行時間が徐々に長くなってきた、という経験がある。よって、最初はユニットテストの条件を満たしているが、システムが大きくなるにつれ実行時間を短く保つことができなくなり、ユニットテストではなくなる、という形となる。
また、依存の章の内容から、自分の書くテストは古典学派ロンドン学派どちらにも属さないことがわかった。つまり、自分は共有依存もプライベート依存も、どちらもテスト・ダブルに置き換えていない。と思ったが、インメモリSQLiteはメソッドの度にリセットしておりテスト間で共有していないのでデータベースはそもそも共有依存になっていなかった。
第3章 単体テストの構造的解析
3.1.2 単体テストにおいて回避すべきこと 同じフェーズを複数用意すること
「複数の振る舞いを検証している」これはだめ。
1つのテスト・ケースの中に、複数の実行フェーズと確認フェーズが存在するのであれば、そのテスト・ケースを分割して、1つの実行フェーズと1つの確認フェーズを持つテスト・ケースを複数作成するようにします。
実践としては、このようにコードを改善する。
さらに、テスト・ケースの理解のしやすさや保守のしやすさの観点から考えると、同じフェーズを複数持つような1つのテスト・ケースよりも、各フェーズが1つしかない複数のテスト・ケースの方が優れています。
改善した先には理解や保守にメリットが生まれる。
3.1.5 確認(Assert)フェーズで確認する項目はどのくらいあれば良いのか?
1単位の振る舞いによって、複数の結果が生じる事はあり得ることであり、1つのテスト・ケースでその結果を全て検証する事は自然なことなのです。
httpレスポンスやデータベースの中身といった複数の観点の確認や、それぞれの観点の中で複数の項目を確認する事はよくやっているが、これは問題ないと言う自信がついた。
3.2 単体テストのフレームワークについて
本書では、単体テストを作成する際、このように考えることを推奨しています。なぜなら、単体テストですべき事は、プロダクション・コードが何をするかを単に列挙することではなく、アプリケーションの振る舞いについてより高いレベルで描写することだからです。そして、理想なのは、単体テストで表現していることが開発者だけでなく、非開発者にも伝わることです。
テストケースにはシナリオ、物語、ストーリー、を語ることが大事。
3.3.2 コンストラクトの利用はテスト・ケースの読みやすさを損なわせてしまう
準備(Arrange)フェーズのコードをコンストラクタに持っていくことのもう一つの欠点はテスト・メソッドが読みづらくなってしまうことです。なぜなら、テスト・メソッドだけを見ても、準備フェーズのロジックがそこにはないため、
だからわざわざファクトリーセットを用意して、テストメソッドの最初に書くことで明示すると言うことか理解ができた。
3.3.3 テストフィクスチャーに関するコードを共有するためのより良い方法
共通的に利用するテスト・フィクスチャの準備に関するコードをプライベートなファクトリ・メソッドに定義することで、テスト・コードの量を減らすのと同時に各テスト・ケースで何が行われるかを完全に理解できるようになります。
3.4.2 指針に従った名前に変えた場合の例
コラム なぜ、テスト対象のメソッド名をテスト・メソッド名に含めるべきではないのか?
テスト対象のメソッド名はテスト・メソッド名に含めるべきではありません。その理由は、単体テストはコールをテストしているのではなく、アプリケーションの振る舞いをテストしているからです。
しかしながら、テストメソッド名に「should be(べきである)」とつける事はよくあるアンチ・パターンの1つです
そのため、テストケースには、事実に基づいた名前をつけるべきであり「べきである(should be)」を「である(is)」に変えなくてはなりません
第2部: 単体テストとその価値
第4章 良い単体テストを構成する4本の柱
どうすれば、価値のあるテスト・ケースを認識できるようになるのか
4.1.1 1本目の柱: 対抗(regression)に対する保護
退行で最悪なのは、扱っている機能が増えるほど、新たな機能を開発するたびに、既存の機能の振る舞いがおかしくなってしまうことです。
自身の経験では、デグレード、デグレなどと呼んでた。これと同じことだと思う。
4.1.2 2本目の柱: ファクタリングへの耐性
ファクタリングへの耐性 = 偽陽性(false positive)をなくす。
- 偽陽性: 火が出ていないのに(コードが正しいのに)火災報知器が鳴る(テストが失敗する)。
- 偽陰性: 火が出ているのに、火災報知器がならない。
偽陽性によってテストを実施すること自体の意味が損われてしまうから
テスト対象のコードが正しく振る舞っているのにもかかわらず、テストが失敗するようなことが続くと、開発者はそのことに慣れてしまい、テスト結果を重要視しなくなるからである。
4.1.3 何が偽陽性(false positive)を引き起こすのか?
偽陽性を生み出す可能性を減らす唯一の方法はテスト・コードをテスト対象の内部的なコードから切り離すことです。
テスト・コードがテスト対象となるコードの詳細と深く結びつくと、リファクタリングへの耐性がなくなる、ということがわかります。
4.1.4 実装の詳細ではなく、最終的な結果を確認する
質の良いテスト:最終的な結果は正しいか?
質の悪いテスト:手順は正しいのか?
4.2 退行(regression)に対する保護とリファクタリングへの耐性との関係
プロジェクトが始まって、すぐに退行に対する保護を備えることが重要となるのに対し、リファクタリングへの耐性はすぐに必要となるわけではありません。
4.2.1 テストの正確性を最大限にすること
たとえば、インフルエンザの検査を受けた人が本当にインフルエンザにかかっている場合、その検査結果は陽性 (positive) となります。
逆に、患者がインフルエンザにかかっていないのであれば、その検査で反応を示してはならず、検査結果は陰性 (negative) とならなくてはなりません。
4.2.2 偽陽性 (false positive) と偽陰性 (false negative) の重要性の違い
それでは、なぜ、プロジェクトの初期段階において、利用性はそれほど大きな弊害にならないのでしょうか?その理由は、プロジェクトの初期段階にはリファクタリングをする必要性がないことにあります。
ほとんどの開発者は、偽陽性のことを問題としては認識していません。なぜなら、多くの開発者は1本目の柱である退行に対する保護だけに目を奪われてしまうからです。
4.4 理想的なテストの探求
覚えておいてほしいのは、テスト・コードを含めたすべてのコードは負債であるということです。
4.4.2 極端な例 #1: E2E (End-to-End) テスト
しかしながら、このような効果があるのにもかかわらず、E2Eテストには大きな欠点があります。それはテストを実行し終えるまでに時間がかかってしまうことです。
4.4.4 極端な例 #3: 壊れやすいテスト
同様に、実行時間が短く、退行を見つけることに優れてはいても、多くの偽陽性を持ち込んでしまうテストも簡単に作成できてしまいます。このようなテストのことを本書では壊れやすいテストと呼んでいます。
4.4.5 理想的なテストに関する結論
しかしながら、現実においてリファクタリングへの耐性を犠牲にすることはできません。なぜならリファクタリングへの耐性は可能な限り備えなくてはならない性質だからです。そのためには作成するテスト・ケースに対して十分な実行速度を持たせなくてはなりません。つまりE2Eテストだけでテスト・スイートを構成するようなことはしてはならないということなのです。
4.5.1 テスト・ピラミッド
テストピラミッドを単体テスト、統合テスト、EZテストの3種類によって構成している。Googleのソフトウェアエンジニアリングの本にあったサイズによる分類分けではない。本書の方が古い?当たり前なのだが、著者が書いた時点での情報なのでより新しい情報は載っていない。この本にはすごく良いことが書いてある。一方でこの本の内容も、いずれ陳腐化していくのだろう。この本以外の本も読んでいくことで、このようなことも感じられる、読書の面白さよ。もしくはそもそも考え方が違う?
4.5.2 ブラック・ボックス・テストとホワイト・ボックス・テスト
ただし、ここで心に留めておいて欲しいのは、テストを分析する際は、ホワイト・ボックス・テストの手法も用いることができるということです。
このように、ブラック・ボックス・テストとホワイト・ボックス・テストを組み合わせることで、テスト・スイートの質をより高められるようになります。
まとめ
偽陽性は、テスト・ケースがテスト対象の内部的なコードと結びつくことで発生する。
リファクタリングへの耐性を排除することはできない。なぜなら、この柱はテスト・ケースに備えるか否かの選択しかできないからである。
第5章 モックの利用とテストの壊れやすさ
一方、古典学派では単体テストのテスト・ケース自体を隔離すべきだと考えており、そうすることで、複数のテスト・ケースを同時に実行できるようにすることを提唱しています。 そのため、古典学派ではテスト・ケース間で共有される依存に対してのみテスト・ダブルを使うようにしています。
5.1.1 テスト・ダブルの種類
テスト・ダブルとは、プロダクション・コードには含まれず、テストでしか使われない偽りの依存として表現するされるすべてのものを包括的に意味するものです。この用語は、映画のスタントマン (英語だと「stunt double」) を語源としています。
モックは、テスト対象システムからその依存に向かって行われる外部に向かうコミニケーション(出力)を模倣し、そして、検証するのに使われる。
スタブは依存からテスト対象システムに向かって行われる内部に向かうコミニケーション(入力)を模倣するのに使われる。
- テスト・ダブル
- モック: メール送信、副作用を発生させる。検証時に使われる。
- スタブ: データベースからデータ取得、副作用は発生しない。検証時に使われない。
このことに加え、モックはテスト対象システムとその依存とのコミュニケーションを模倣するだけではなく、検証も行うのに対し、スタブはテスト対象システムとその依存とのコミュニケーションの模倣だけしか行わないと言う違いもあります。
5.1.3 スタッフとのコミュニケーションは検証しない
テストの観点において、テスト対象のコードが正しい結果を返す限り、最終的な結果をどのように作成しているのかについては目を向ける必要は無いのです。
スタブに対しては、いかなるコミュニケーションであっても、検証すべきではありません。」
5.1.5 モックとスタブがそれぞれどのようにコマンドとクエリに結びつくのか?
モックとスタブの考え方は、コマンド・クエリ分離 (Command Query Separation: CQS) の原則との関連性があります。 コマンド・クエリ分離の原則では、次の図5.3で示すように、すべてのメソッドはコマンドかクエリのどちらかになるべきであり、両方の性質を持つべきではない、ということを提唱しています。
- コマンド(副作用有り、戻り値無し) → モック
- クエリ(副作用無し、戻り値有り) → スタブ
5.2.1 観察可能な振る舞いと公開されたAPIとの違い
理想なのは、システムが公開しているAPIが観察可能な振る舞いと一致し、その一方で、そのシステムのすべての実装の詳細がクライアントから完全に隠れるようになっていることです。
5.2.3 きちんと設計されたAPIとカプセル化
結局のところ、カプセル化が目指しているのは単体テストと同じソフトウェア開発の持続可能な成長なのです。
これと同じような原則に「尋ねるな命じよ(Tell, Don’t Ask)」というものがあります。
5.3.2 システム内コミニケーションとシステム間コミニケーション
そして、その観察可能な振る舞いはテスト対象のアプリケーションが常に尊守しなければならない契約の一部なのです(図5.12)。
そしてこのようなシステム間コミニケーションを検証するのに効果を発揮するのがモックになります。しかしながら、もし、モックをシステム内コミニケーションの検証に使ってしまうと、テストが実装の詳細と深く結びついてしまい、リファクタリングへの耐性を失うことになります。
5.4.1 モックに置き換える必要のないプロセス外依存
ところが、このような完全に制御可能なプロセス外依存をモックに置き換えて検証をすると、そのテスト・ケースは壊れやすくなってしまいます。
そのため、このようなデータベースとアプリケーションは、1つのシステムとして扱われなければなりません。
まとめ
そして、コマンドとクエリをテスト・ダブルに置き換える場合、コマンドの代わりになるのがモックであり、クエリの代わりとなるのがスタブである。
← コマンド・クエリ分離 (Command Query Separation: CQS)
第6章 単体テストの3つの手法
ユニットテスト側の視点から、プロダクションコードのモダンなプログラミングのやり方にも踏み込んでいる。良い。
6.1.3 コミュニケーション・ベース・テストとは?
古典学派はコミュニケーション・ベース・テストよりも状態ベース・テストの方を行うのに対し、ロンドン学派はコミュニケーション・ベース・テストの方を好みます。
6.3.1 関数型プログラミングとは?
副作用とはメソッド・シグネチャには表現されていない出力のことである。つまり、副作用は隠れた出力を意味する。
つまり、例外はメソッド・シグネチャには定義されていない隠れた出力を意味することになる。
つまり、このようなことをするメソッドには隠れた入力が存在することになる。
6.3.2 関数型アーキテクチャとは何か?
関数型プログラミングが目標としている事は、副作用を完全に取り除くことではなく、ビジネス・ロジックを扱うコードと副作用を起こすコードを分離することにあります。
そのため、両方の責務が1つのメソッドに混ざってしまうと、複雑さが倍増し、コードを長い間保守することが難しくなってしまいます。
言い換えると、可変殻は可能な限り指示されたことだけしか行わないような作りにする、ということです。 こうすることで、単体テストは出力値ベース・テストを用いて関数的核だけを検証できるようになります。 一方、可変殻の検証は、テストケースが単体テストよりも少ない統合テストに任せるようにします。
6.3.3 関数型アーキテクチャとヘキサゴナル・アーキテクチャの比較
関数型アーキテクチャとヘキサゴナル・アーキテクチャには多くの類似点があり、両方とも関心の分離 (separation of concerns) が基盤となっています。
さらに、他の類似点として、依存の流れが一方向になっているということがあります。
関数型アーキテクチャでは、すべての副作用を関数的核の外に出し、ビジネス・オペレーションの最初や最後に持ち込むようにします。 一方、ヘキサゴナル・アーキテクチャでは、ドメイン層内に限定される限り、副作用を起こすことが許されています。
6.4 関数型アーキテクチャ及び出力値ベース・テストへの移行
どのように関数型アーキテクチャとなるように、リファクタリングをするのか、ということを見ていきます。 このファクタリングは次の2つの移行を順を踏んで行うことで実施されます:
- プロセス外依存の利用からモックの利用への移行
- モックの利用から、関数型アーキテクチャの利用への移行
6.4.1 サンプル・プロジェクト(訪問者記録システム)について
テストが行いづらくなっている原因は、ファイル・システムを直接扱っていることにあります。なぜなら、このファイルシステムは共有依存であるため、 もし、他のテスト・ケースもこのファイル・システムに対して何らかの操作を行うのであれば、そのことに関して何らかの対策をしなくてはならないからです。
6.4.2 モックを用いることによる単体テストとファイル・システムの分離
ここで心に留めておいてほしいことは、これが正当なモックの使い方である、ということです。 この訪問者記録システムが作成するファイルはエンド・ユーザーからもアクセスされることが想定されるものです
そのため、ファイル・システムとのコミュニケーション、および、その際に発生する副作用は、訪問者記録システムの観察可能な振る舞いの1部となります。
先程のリスト6.11のテスト・ケースは、準備 (Arrange) フェーズに記述されたコードが修正前よりも複雑になっているため、保守コストの観点で言えば、理想的な実装にはなっていません。
6.4.3 関数型アーキテクチャへのリファクタリング
要は、Persister クラスが可変殻(mutable shell)としての役割を担う一方、AuditManager クラスが関数的角 (functional core) としての役割を担うようにするのです。
そしてこのクラスには、条件分岐 (if文) はありません。つまり、ビジネス・ロジックに関するすべての複雑さはAuditManagerクラスが担うことになるのです。まさに、このことがビジネス・ロジックと発生する副作用の分離なのです。
関数的核の外にあるコード (可変殻) は、複雑なことをしなくても済むようになります。
図 6.15 P198 図 6.14 と流れが同じ!
6.5.1 関数型アーキテクチャの導入が難しい場合
第2章で見たように、協力者オブジェクトとは、次の性質を1つ以上持つ依存のことです:
- 可変である(状態を変えることができる)こと
- まだメモリ上にはないデータの橋渡しをするもの(共有依存)であること
NOTE: 関数的核 (functional core) に属するクラスは協力者オブジェクトと共に処理を行うのではなく、その協力者オブジェクトによって得られた結果(つまり、値)を用いて処理をするようにします。
6.5.2 パフォーマンスに関する欠点
しかしながら、関数型アーキテクチャとなるように修正したバージョンでは、訪問者の記録を追加するためには、最初にすべてのファイルを読み込まなくてはならないようになっています (関数型アーキテクチャの「入力データの収集→決定→実行」の形式に準拠させるため)。
6.5.3 コードベースが大きくなってしまう欠点
関数型アーキテクチャでは、関数的核 (functional core) と可変殻 (mutable shell) を明確に分離するようになっているため、最初は実装しなくてはならないコード量が多くなります。しかしながら、最終的には、コードの複雑さが減るため、保守はしやすくなります。
このように出力値ベース・テストではないテスト・ケースを用いる事は何も問題がないのです。本書がこの章で読者に求めていることは、すべてのテスト・ケースを出力値ベース・テストにすることではなく、できるだけ多くのテスト・ケースを出力値ベース・テストにする、ということなのです。この違いはわずかな違いですが、重要な違いです。
まとめ
出力値ベース・テストとは、テスト対象システムに入力値を与え、そこで生成された結果を検証する単体テストの手法のことである。
状態ベース・テストとは、テスト対象システムと協力者オブジェクトの状態を検証する単体テストの手法のことである。
コミュニケーション・ベース・テストとはモックを使ってテスト対象システムと協力者オブジェクトのコミニケーションを検証する単体テストの手法のことである。
出力値ベース・テストが単体テストの3つの手法の中で、もっとも質の高いテストケースを作成できる。その理由は、テスト・ケースが実装の詳細と結びつくことがあまりないため、リファクタリングへの耐性がテスト・ケースに自然と備わるからである。
たとえば、単体テストを行えるようにするため、プライベートの状態を公開するような事は決して行ってはならない。
そのためには、検証の対象となるコミニケーションがアプリケーションの境界を超えて行われ、かつ、外部から確認できる副作用を発生させる場合にのみ、コミニケーション・ベース・テストを用いるようにする。
関数型プログラミングを導入することで達成したいことは、ビジネス・ロジックと副作用を分離することである。
関数型アーキテクチャは、副作用をビジネス・オペレーションの最初や最後に持っていくことで分離を実現しようとするものである。
関数的核 (functional core) は決定を下すものであり、可変殻 (mutable shell) は、関数的核に対して入力値を提供したり、関数的核で下された決定に基づいて副作用を起こす処理を行ったりするものである。
関数型アーキテクチャを導入すると、パフォーマンスは犠牲になるが、そのかわり、保守はしやすくなる。
もし、システムが単純でそこまで重要性がないのであれば、関数型アーキテクチャを導入するのに必要な初期コストの方が後で得られる価値よりも高くなる可能性がある。
第7章: 単体テストの価値を高めるリファクタリング
なぜなら、単体テストとそのテスト対象となるコードはお互いに影響しあっており、プロダクション・コードの改善なしに価値のある生み出すことができないからです。
この章では、このアプローチを幅広い範囲のアプリケーション (関数型アーキテクチャが適用できないアプリケーションも含む) に浸透させるにはどうするのか、ということを見ていきます。
7.1 リファクタリングが必要なコードの識別
こうなる理由は、テスト・コードとプロダクション・コードは本質的に影響しあうものだからです。 そこで、この7.1節では、リファクタリングの方向性を見極めるのに必要なプロダクション・コードの種類について学び、テスト対象となるコードはどのように分類するのかを見ていきます。
7.1.1 4種類のプロダクションコード
すべてのプロダクション・コードは、次の2つの視点で分類できます: ・コードの複雑さ、もしくはドメインにおける重要性 ・ 協力者オブジェクトの数
単体テストを行う価値が最も高いプロダクション・コードは複雑なコード、もしくはドメインにおける重要性が高いコードです。なぜなら、そのようなコードをテストすることで、退行 (regression) に対する保護が備わるようになるからです。
第2章で述べたように、協力者オブジェクトとは、可変 (mutable) もしくはプロセス外の依存のことです (もしくはその両方の性質を持つ依存) 。
そのため、この章では、どのようにこのジレンマを回避するのか、ということを主に考えていきます。
NOTE: 質の悪いテスト・ケースを作成するくらいなら、そのようなテスト・ケースをまったく作成しない方がよいでしょう。
7.1.2 質素なオブジェクト (Humble Object) を用いた過度に複雑なコードの分割
多くの場合、テストをすることが難しくなるのは、テスト対象のコードがフレームワークとなる依存に直接結びつく場合です (図7.3) 。
このビジネス・ロジックに関する責務と連携の指揮に関する責務は、それぞれコードの深さ (複雑さや重要性) 、コードの広さ (協力者オブジェクトの数) として考えることができます。
コントローラがいかに多くのオブジェクトを指揮していても、そのコントローラは複雑さを持ってはならない。一方、ドメイン・クラスは逆となるようにしなくてはならない。
この場合、集約内のクラスはお互いが密接に結びついてしまうのですが、集約同士は疎結合の関係を築くことになります。
ただし、ここで注意して欲しいのは、ビジネスロ・ジックとなる部分と連携を指揮する部分との分離を維持する理由はテストを行いやすくすることだけではない、ということです。このような分離はプロダクション・コードの複雑さを解決するのにも役立ちます。
このようにテストを行いやすい設計にすることが単にテストをしやすさに繋がるだけではなく、保守のしやすさにも繋がることに私はいつも魅力を感じています。
7.2 単体テストに価値を持たせるためのリファクタリング
今回は、質素なオブジェクト (Humble Object) を用いて、この関数型アーキテクチャへの移行をすべてのエンタープライズ・レベルのアプリケーションに適応できるような汎用化を行っていきます。
7.2.5 3回目のリファクタリング: 新たな Company クラスの導入
なぜなら、このようにリファクタリングすることで、単体テストはプロセス外依存を検証しなくて済むようになり、その結果コミニケーション・ベーステストを行う必要がなくなるからです。そうなると単体テストで行うすべての検証は、メモリ上のオブジェクトに対して出力値ベース・テストと状態ベース・テストだけを用いて行えるようになります。
7.3.3 事前条件をテストすべきか?
そのため、このようなセーフティー・ネットを設けておくことで、ソフトウェアは早期失敗 (Fail Fast) し、間違いが広がっていくことやその間違いがデータベースにまで及ぶことを防げるようになります。
しかしながら、本書が推奨する一般的な指針は、もし、事前条件がドメインにおいて重要であれば、その事前条件はテストされるべき、というものです。
7.4 コントローラにおける条件につきロジックの扱い
ビジネス・ロジックのコードと連携を指揮するコードの分離を最も行いやすいのは、1つのビジネス・オペレーションが次の3段階の流れになっている場合です (図 7.10) : 1. ストレージからのデータの取得 2. ビジネスロジックの実行 3. 変更されたデータの保存
しかしながら、これらの手順を完璧に遵守できない場合もよくあります。たとえば、前章で述べたように、決定を下す過程の中で、途中で得た結果を使ってプロセス外依存から新たにデータを取得しなければならないような場合です (図 7.11) 。
図 7.11 ビジネス・オペレーションを実施している最中にプロセス外依存へのアクセスが要求される場合、ヘキサゴナル・アーキテクチャはうまく機能しない
こうなると、選べるものは3つ目の選択肢 (決定を下す過程をさらに細かく分割する) だけになります。ただし、この選択肢を選ぶと、コントローラのコードは複雑になってしまい、その実装が過度に複雑なコードの領域に近づくことになります。しかしながら、この問題はある程度であれば対処することが可能です。
7.4.1 確認後実行 (CanExecute/Execute) パターンの適用
コントローラーがより複雑になっていることへの対策として、最初にできる事は確認後実行 (CanExecute/Execute) パターンを採用することです。このパターンはドメイン・モデルにあるビジネス・ロジックがコントローラに流出することを防ぐためのものです。
そうなると、すべての決定は User クラスで下されていることとなり、コントローラーはその決定に基づいた振る舞いをしているだけになります。
7.4.2 ドメイン・モデルの状態を追跡するとドメイン・イベントの利用
しかしながら、アプリケーションで何が起こったのかを正確に外部システムに伝えなければならないため、その経緯を知ることが重要になる場合もあります。
そこで、このような複雑さをコントローラに持ち込むことを避けるため、ドメイン・モデルで起こった重要な状態の変更を把握できるようにし、その変更を発生させたビジネス・オペレーションが終わったあとに、プロセス外依存に対して変更があったことを伝えるようにします。まさにこのような場合使えるのがドメイン・イベントなのです。」
しかしながら、現実のプロジェクトにおいて、プロセス外依存をドメイン・モデルに渡してしまうと、アプリケーションがプロセス外依存を不必要に呼び出すことを防ぐことが難しくなってしまいます。
このドメイン・イベントを実装の観点から見ると、ドメイン・イベントとは、外部システムに伝えなくてはならないデータを含んだクラスのことになります。
7.5 結論
それでは、この章の主題である外部システムに対して発生させる副作用をドメイン層から取り除く抽象化について振り返っていきましょう。 この章では、このような副作用を起こす処理をビジネス・オペレーションの最後まで実行させないようにし、それまでの間メモリ上で作業を行わせるような抽象化を行うことで、プロセス外依存を巻き込まずに単体テストを行えるようになることを見てきました。さらに、ドメイン・イベントをコレクションに追加することがメッセージ・バスへメッセージを送信することの抽象化であること、そして、ドメイン・クラスの状態を変更することがデータベースの状態を変更することの抽象化であることも見てきました。
また、ビジネス・ロジックをコントローラに持ち込んでしまうことを避けられないのと同じように、ドメイン・クラスからすべての協力者オブジェクトを完全に取り除けることも滅多にありません。しかしながら、このことに関して神経質になる必要はありません。なぜなら、仮に、1つや2つ、場合によっては、3つの協力者オブジェクトがドメイン・クラスに含まれていたとしても、それらの協力者オブジェクトがプロセス外依存でない限り、ドメイン・クラスが過度に複雑なコードになることはないからです。
このように、観察可能な振る舞いと実装の詳細との関係は玉ねぎの層のように考えることができます。各層のテストは1つ上の層の視点で行い、テスト対象となる層がその下にある層と何をしているのかについては意識しないようにします。
まとめ
決定を下す過程をさらに細かく分割することは、各性質の長所と短所を考慮した最善のトレード・オフの結果である。次の2つの設計パターンを用いることで、コントローラが複雑になってしまうことへの対策が行える:
- 確認後実行(CanExecute/Execute)パターンとは、何かを実行するメソッド(Execute メソッド)に対して実行可能なのか否かを確認するメソッド(CanExecute)を用意することで、対象の処理を正しく実行するための事前条件が必ず満たされることを保証する設計とである。このパタンを用いることで、処理を実行するメソッドが呼び出される前に必ず事前条件の確認を行うメソッドが呼び出されるようになるため、コントローラが決定を下す責任を本質的に取り除くことになる。
- ドメイン・イベントを導入することで、ドメイン・モデルで発生する重要な状態を変更を追跡できるようになる。そして、発生したドメイン・イベントをもとにプロセス外依存へ呼び出しを行うようにする。このように、ドメイン・イベントを用いることで、コントローラから状態の変更を追跡する責務を取り除けるようになる。
第3部: 統合(integration)テスト
第8章: なぜ、統合(integration)テストを行うのか?
8.1.1 統合テストの役割
見ての通り、単体テストはドメイン・モデルを検証し、統合テストはドメイン・モデルとプロセス外依存とを結びつけるコードを検証します。
しかしながら、ほとんどのアプリケーションには、モックに置き換えるべきではないプロセス外依存が存在します。その依存とはデータベースのことです。
つまり、すべてのテスト・ケースにおいて、検証対象となるコードは、ドメイン・モデル(または、アルゴリズム)、もしくは、コントローラのどちらかだけである、ということです。
8.1.2 テストピラミッドの振り返り
また、単体テストと統合テストの適切なテストケースの割合はプロジェクトによって異なるのですが、一般的に、単体テストはビジネス・シナリオにおける異常ケース (edge case) をできるだけ多く検証するのに対し、結合テストは1件のハッピー・パス (happy path) と単体テストでは検証できないすべての異常ケースを検証することが適切だと考えられています。
8.2.1 2種類のプロセス外依存
管理下にある依存 (managed dependency) – この依存は、テスト対象のアプリケーションが好きなようにすることができるプロセスが依存である。 …略… 管理下にある依存の典型的な例に、テスト対象のアプリケーションしかアクセスしないデータベースがある。
管理下にない依存 (unmanaged dependency) – この依存は、テスト対象のアプリケーションが少ないようにすることができない。プロセスが依存である。 …略… 管理下にない依存の典型的な例にメールサービスやメッセージバスがあり、両方とも他のアプリケーションに見える副作用を発生させる。
重要 : 管理下にある依存に対しては、実際のインスタンスを使うようにし、管理下にない依存に対してはモックを使うようにしましょう。
8.2.3 統合テストで、実際のデータベースを使えない場合
そのため、もし、実際のデータベースを使って検証できないのであれば、そのことに関する統合テストを作成せず、ドメイン・モデルの単体テストを作成することだけに専念するようにしましょう。そして、常に最新の注意を払って、すべてのテスト・ケースを精査し、もし、あまり価値のないテスト・ケースを見つけた場合は、そのテスト・ケースをテスト・スイートから取り除くようにしましょう。
8.3.4 統合テストの作成
データベースの状態を確認する際、検証するデータが入力値として使ったデータとは異なる経由で取得したものにする事はテストにおいて重要なことです。
8.4 インターフェースを使った依存の抽象化
単体テストに関して誤解を生んでいることの1つにインターフェイスの利用があります。なぜ、インターフェースを使うのか、ということに関して間違った認識をしている開発者は多く、その結果、インターフェイスが過度に使われてしまうことが多々あります。
この章を読んでいくのが楽しみ。自分も間違って使っていそう。使い過ぎなので抑えようしようと言うことだな。
8.4.1 インターフェイスと疎結合の関係
まずインターフェイスの実装クラスが1つしかないのであれば、そのインターフェイスは抽象ではありません。 …略… 本来、抽象化とは、発見することであり、作り出すことではないのです。 …略… そのため、インターフェイスが本当の抽象になるためには、2つ以上の実装クラスが存在しなくてはなりません
一方、もう一つの認識(既存のコードを変更することなく、新しい機能を追加できるようになる)もまた間違った認識です。なぜなら、このような理由でインターフェイスを用意することは設計のより根本的な原則である YAGNI (You Aren’t Gonna Need It) 原則から外れることになるからです。
TIP: コードを書くことはコストのかかる問題解決の方法です。そのため、問題を解決するのに記述しなくてはならないコードは少なくて簡潔なほど良い、ということになります。
8.4.2 なぜ、プロセス外依存にインターフェイスを使うのか?
それでは、インターフェイスを実装するクラスが1つしかないのにもかかわらず、なぜ、プロセス外依存に対してインターフェイスを使うのでしょうか?それはモックを作れるようにするため、という非常に実践的で現実的な理由が答えになります。
このことを煮詰めていくと、管理下にない依存に対してのみインターフェースを用意する、という結論に至ります。一方、管理下にある依存に対しては具象クラスを使うようにし、その具象クラスをコントローラに明示的に注入するようにします。
例えば、管理下にない依存というのはメール送信、管理下にある依存というのはデータベースへのアクセス。
8.5.1 ドメイン・モデルの境界を明確にする
統合テストがテスト対象とするのはコントローラーです。そのため、ドメイン・クラスとコントローラとの境界が明確になっていれば、単体テストと統合テストの区別をしやすくなります。
8.5.2 アプリケーションを構成する層を減らす
このような抽象化が過度に増えてしまうと、単体テストや統合テストも行いづらくなってしまいます。なぜなら、間接参照の層が多いコードベースだとコントローラとドメイン・モデルの境界が曖昧になることがよくあるからです。(そして、第7章で述べたように、この境界の存在は効果的なテストを行うために必要なものです)。
そのため、間接参照の層はできるだけ少なくなるように努めなくてはなりません。バックエンドのシステムであれば、ほとんどの場合、次の図8.10で示すように、ドメイン層(ビジネス・ロジックを含む層)、アプリケーション・サービス層(コントローラの層)、インフラ層の3つの層だけで充分なはずです。
8.5.3 循環依存を取り除く
循環依存もまた、過度に多い間接参照の層と同じように、コードを読んで理解することを難しくし、開発者に対して膨大な認知的負荷を与えるものです。
そうなると、インターフェイスを用いて循環依存を隠すようなことをするよりも、その循環依存を完全に取り除いてしまう方が良いでしょう。
もちろん、コードベースからすべての循環依存を取り除くことはできないことのほうが多いでしょう。
8.5.4 1つのテスト・ケースに複数の実行 (Act) フェーズを用いる場合
このように、複数の実行フェーズを持ったテスト・ケースの作成を許容できるのは、開発者がテストをするのに好きなようにプロセス外依存を扱えない場合だけです。まさに、このことが単体テストに対して複数の実行フェーズを持ったテスト・ケースを作ってはならない理由なのです。(なぜなら、単体テストでは、プロセス外依存を扱うべきではないからです)。さらに、統合テストの場合でさえも、複数の実行フェーズを持つテスト・ケースを作ることに意味があることは滅多にありません。実際のところ、複数の実行フェーズを持つようなテスト・ケースを作成するのは、E2Eテストに分類されるテストで行うことがほとんどです。
8.6.1 そもそも、ログ出力をテストすべきか?
その意味において、ログ出力は他の機能と何も違うところはありません。まず、ログ出力は最終的にテキスト・ファイルやデータベースなどのプロセス外依存に対して副作用を起こすものです。そして、もし、このような副作用を外部(ユーザ、アプリケーションのクライアント、非開発者)からも見られることを意図しているのであれば、このログ出力は観察可能な振る舞いであり、そのため、テストもされなくてはなりません。一方、もし、出力されたログを見るのが開発者だけである場合、そのログ出力は実装の詳細であり、開発者は外部に気を使うことなくそのログ出力を修正できます。そのため、この場合のログ出力はテストされるべきではありません。
- サポート・ログ – システムのサポート・スタッフやシステム管理者によって見られることを意図した特定のイベントを記録するログ。
- 診断ログ – 開発者がアプリケーション内で何が起こっているのかを把握できるようにするためのログ
何をログ出力するべきなのか?今までしばしば悩んでいたが、答えの手がかりがひとつ見つかったように思う。→ ログ出力するべきものはビジネス要件の対象であるとし、決める。ビジネス要件と定めたログ出力は、メール送信と同列同等とみなすと言える、と考えると筋が良さそう。
テストの事だけではなく、システムの作り方に対する学びや考えるきっかけを得られる。とても良い本だ。
8.6.2 どのようにログ出力をテストすべきか?
ログ出力は、プロセス外依存に対して副作用を発生させる機能であるため、テストの際はプロセス外依存とのやりとりを行う他の機能と同じルールが適用されることになります。つまり、モックを使って、アプリケーションとログ・ストレージとのあいだで行われるやりとりを検証することになります。
そこで、すべてのサポート・ログが各メソッドとして定義された DomainLogger クラスを作成し、この DomainLogger クラスからサポート・ログを出力するようにします。
つまり、User クラスでプロセス外依存である DomainLogger クラスを使うと、User クラスはテストや保守をすることが難しい過度に複雑なコードに属するコードになってしまうのです。(詳細については第7章を参照)。ただし、この問題は、ドメイン・イベントを投入して、メール・アドレスが変更されたことを外部システムへ通知するようにした時と同じ方法(詳細は第7章を参照)で解決できます。
8.6.3 どのくらいのログを出力すれば充分なのか?
もちろん、サポート・ログはビジネス要件であるため、確実に出力しなければなりません。
診断ログの場合、重要なのは過度に出力しないことです。なぜなら、診断ログの過度な出力は次の課題を抱えることになるからです。
ほとんどの場合、ドメイン・クラスで出力させているログをコントローラに出力させるようなリファクタリングは安全に行えます。
つまり、デバッグが終わったら、そのログ出力のコードを取り除いてしまうのです。理想なのは、想定外の問題が起こったときにだけ、診断ログを使うようにすることです。
デバッグ用のログは、また使うかも、と考え残しておきたくなる誘惑に駆られるのだが、この本では取り除くことを推奨している。確かに、他メンバーのデバッグログはレビュー時に認知負荷の上昇要因となったり、忘れた頃にコードを見るときに邪魔と感じることが多いように思う。
8.7 結論
すべてのプロセス外依存とのコミュニケーションに対して、そのコミニケーションが観察可能な振る舞いの一部なのか、それとも実装の詳細なのか、ということを考えるようにしましょう。その意味においては、ログ出力も同じです。
まとめ
統合テストは、単体テストよりも優れた退行に対する保護とリファクタリングへの耐性を提供する一方、単体テストは統合テストよりも優れた保守のしやすさと迅速なフィードバックを提供する。
単体テストでは、ビジネス・シナリオにおける異常ケースを可能な限り多く扱うようにする。一方、統合テスト、では1件のハッピー・パス、および、単体テストでは扱えなかった異常ケースをできるだけ多く扱うようにする。
**早期失敗(Fail Fast)**とは問題(バグ)が発生したら、すぐに処理を失敗させることを提唱している原則であり、統合テストの代わりとして実際に使える原則である。
**管理下にある依存(managed dependency)**とは、テスト対象のアプリケーションを経由することでしかアクセスできないプロセス外依存のことである。そのため、管理下にある依存とのやりとりを外部から見ることはできない。管理下にある依存の例としてよくあるのが、テスト対象のアプリケーションしかアクセスしないデータベースである。
**管理下にない依存(unmanaged dependency)**とは、他のアプリケーションからもアクセスされるプロセス外依存のことである。そのため、管理下にない依存とのコミュニケーションは、外部から見ることができる。管理下にない依存の例としてよくあるのが、メール・サービスやメッセージ・バスである。
統合テストの場合、管理下にある依存に対しては実際の依存を使い、管理下にない依存に対してはモックを使うようにする。
サポート・ログは、システムのサポート・スタッフやシステム管理者によって見られることを意図したログであり、アプリケーションの観察可能な振る舞いの一部に分類される。一方、診断(diagnostic)ログは、開発者がアプリケーションの中で何が起こっているのかを把握するために利用するログであり、実装の詳細に分類される。
診断ログの出力はテストされるべきではない。またサポート・ログとは異なり、診断ログをドメイン・モデルで直接出力しても問題は無い。
すべての依存(ログ出力オブジェクトも含む)は、常に、コンストラクタやメソッドの引数を経由して明示的に注入されるようにすべきである。
引用してきた箇所がそのまままとめになっている、上手く伝えたいことを拾えている感じがした。が、自惚れ過ぎかも。
第9章: モックのベスト・プラクティス
そこで、この章では、成功に到達するまでの残りの道のりについて見ていきます。つまり、ここではモックを用いた統合テストを作成する際に、そのテスト・ケースに退行(regression)に対する保護とリファクタリングへの耐性を最大限に備えさせるには、さらに何をすべきなのか、ということを見ていきます。
9.1.1 アプリケーションの境界を超えて行われるコミニケーションの検証
TIP : 管理下にない依存への呼び出しがアプリケーションの境界を越えるまで、その呼び出しには様々なコンポーネントを経由することになります。そのため、外部との境界に最も近いコンポーネントをモックに置き換えるようにします。そうすることで、外部システムとのやりとりにおいて、後方互換を保てるようになります。まさに、この後方互換の保証がテストに対して求められていることであり、モックを導入する目的なのです。
9.2.1 モックの利用は統合テストに限定する
このベスト・プラクティスは、ビジネス・ロジックに関するコードと連携を指揮するコードとを分離すると言う基本原則(第7章を参照)から来ています。
9.2.2 1つのテスト・ケースには、複数のモックを持たせてはならない、という誤解
しかしながら、単体テストの「単体」が意味する事は、「1単位のコード(a unit of code)」ではなく、「1単位の振る舞い(a unit of behavior)」です。
9.2.3 モックの呼び出し回数を常に確認する
管理下にない依存とのコミュニケーションを検証する際に重要な事は次の2つのことを両方とも保証することです: – 想定する呼び出しが行われていること – 想定しない呼び出しは行われていないこと
9.2.4 モックの対象になる型は自身のプロジェクトが所有する型のみにする
このようなことをするのには、次の理由があります:
- サード・パーティ製のライブラリが実際にどのように機能しているのかを深く知る事は滅多にできないから。
- サード・パーティ製のライブラリ自体が利用可能なインターフェースを既に提供していた場合、そのインターフェースをモックに置き換える対象にすると、モックの振る舞いとサード・パーティ製のライブラリの実際の振る舞いとが一致することを保証しなくてはならなくなり、リスクを伴うことになるから。
- アダプタを挟むことで、サード・パーティ製のライブラリに含まれるビジネス的に本質ではない。技術的な詳細を隠蔽的できるようになり、さらに、自身のアプリケーションの用語を用いてライブラリとの関係を定義できるようになるから。
ただし、ここで注意してほしいのは、自身のプロジェクトが所有する型のみをモックに置き換える、という指針は同じプロセス内の依存に対しては**該当しない、**と言うことです。
第10章: データベースに対するテスト
10.1 データベースをテストするのに必要な事前準備
もし、あなたが統合テストを作ることがなくても、ここで見ていくプラクティスの中から学べる事はたくさんあると本書では考えています。
10.1.1 ソースコード管理システムを用いたスキーマの管理
一方、もし、スキーマに関する変更をすべてソースコード管理システムに格納していれば、真実を示す情報源は1つだけとなり管理がしやすくなります。さらに、プロダクション・コードの変更に伴ったデータベースの変更も記録できるようになります。
10.1.2 スキーマに参照データ(reference data)を含めること
**定義: 参照データ(reference data)**とはアプリケーションを適切に機能させるために、事前に用意しなくてはならないデータのことです
参照データはアプリケーションにとって必要不可欠なものであるため、テーブルやビューなどのスキーマと共にSQLの分のINSERT文の形でソースコード管理システムに格納するのが良いでしょう。
10.1.3 開発者ごとに個別のデータベース・インスタンスを用意する
- 複数人の開発者が同時にテストを実施すると、お互いのテストに影響が出てしまう。
- 後方互換のない変更を加えるときに、他の開発者の作業を止めてしまう。
10.1.4 データベースに対する変更の本番環境への反映:状態ベース VS. 移行ベース
移行ベースを用いる場合、開発者自身が本番環境に変更を反映するSQLスクリプトを作成します。
このように、移行ベースを用いた場合、データベースがどのような状態に変わったのかではなく、データベースに対して何を行ったのかがソースコード管理システムで管理されることになります。
**定義: データ・モーション(data motion)**とは既存のデータの形状を変え、新しくなったスキーマにそのデータが合うようにすることを指します。
しかしながら、ほとんどのプロジェクトにおいて、データ・モーションの取り組みの方がマージ競合の対処よりもはるかに重要となるのが普通です。なぜなら、アプリケーションが本番環境にリリースされてしまうと、本番環境のデータベースは簡単には破棄できないデータを常に持つことになるからです。
こうなる理由は、スキーマ自体が目的であるのに対し、(つまりスキーマをどのように変更するのかの解釈は一方向にしかならないのに対し)、データは文脈に依存するものだからです。
スキーマ自体は目的、データは文脈に依存、この言い回しが何か好き、印象的だった。意味は理解しきれていないけれども。
つまり、適切な変換を行うためには、開発者がドメイン特有のルールを自分の手で適用しなくてはならないのです。
しかしながら、アプリケーションがまだ本番環境にリリースされていないのであれば、状態ベースは十分に利用可能なテクニックです。
これは実践している。初回の本番リリース前まではマイグレーションファイルを直接書き換えている。
10.2.1 プロダクション・コードにおけるデータベース・トランザクションの管理
しかしながら、データの変更を伴うビジネス・オペレーションの場合、すべての更新がアトミック(atomic)に行われないと、データ不整合を生み出す可能性が出てきます。
定義: アトミックの更新とは、複数の更新が一連のつながりを持って行われなくてはならない場合に、処理の途中で何も問題がなければ、すべてのデータの変更をデータベースに反映し、その逆に、処理の途中で何らかの問題が発生したら、どの変更もデータベースには反映せずに処理を終える、という二者択一の更新のことを指します。
**定義: 単位作業(Unit of Work)**とは1つのビジネス・オペレーション中でデータの変更が発生するオブジェクトを全て保持し、そのビジネス・オペレーションが完了するときに、それらのオブジェクトに対して行われたデータの変更(作業)を1単位にまとめてデータベースに反映するパターンのことです(このことがまさにこのパターンの名前の由来となっています)。
しかしながら、リレーショナル・データベースではないデータストアでは、データ不整合への対策を別の観点で行います。その対策とは、1つのビジネス・オペレーションの中で複数のドキュメントを変更しなくても済むようなドキュメントの設計を行うことです。
10.3 テスト・データのライフ・サイクル
このことから、統合テストでは、テスト・ケースが自身で用意していないデータベースの状態に依存すべきではない、という結論に至ります。
10.3.1 統合テストにおけるテスト・ケースの実行: 同時に複数実行すべきか、もしくは、1つずつ実行すべきか?
そのため、統合テストのパフォーマンスを改善するために、多くの時間と労力を費やして、複数のテストケースを無理して同時に実行するようにするよりは、テスト・ケースを1つずつ実行して行く方が現実的です。
扱える個別にデータベース・インスタンスを持てるようにすることの方が現実的なのです。ただし、本書は、Dockerを使って、各開発者が1つのデータベースインスタンスを扱えるようにすることに異を唱えているわけではありません。本書が異を唱えているのはDockerを使った未熟な並列化に対してであり、Dockerの使用自体ではないのです。
10.3.2 統合テストでのデータの後始末
TIP: データの後始末を行うスクリプトでは、通常データは全て取り除かれるのですが、参照データは消して取り除かれないようにしなくてはなりません。なぜなら、参照データの変更はスキーマと共に移行でしか行ってはならないからです。
10.3.3 メモリ内(in-memory)データベースの使用に関する問題
統合テストで使われるデータベースは共有依存ではないため、(そのデータベースがプロジェクト内で唯一の管理下にある依存である、という前提の場合、)そのデータベースをメモリ内データベースに置き換えることで、統合テストを実質的に単体テストのように扱えるようになります。これは前述のコンテナを使ったアプローチに似ています。
しかしながら、これらの効果があるのにもかかわらず、本書では、テストの際に、メモリ内データベースを使うことを推奨していません。その理由は、メモリ内データベースは機能的において一般的なデータベースと異なる部分があるからです。このことは、本番環境とテスト環境で行うことが異なると適切なテストが行えなくなる問題と同じです。
普段からSQLiteのインメモリデータベースを使って統合テストを行っている。本番環境と全く異なるデータベースを使っていることから、常に偽陽性偽陰性が起こりうることを意識しなければならない。ただ、とても早くテストが実行できるメリットがあるので、これからも使い続けると思う。
TIPS: テスト環境で使用するデータベース、管理システム(DataBase Management System: DBMS)と本番環境で使用するデータベース管理システムは同じ種類のものを用意しなくてはなりません。多くの場合、バージョンやエディッションが違っても、そこまで気にする必要は無いのですが、ベンダーに関しては必ず同じベンダーにしなくてはなりません。
10.4 テスト・コードの再利用
そこで、この10.4節では、テスト・ケースの準備フェーズ、実行フェーズ、確認フェーズの3つのフェーズにおいて、それぞれどうすればコード量を減らすことができるのか、という事について見ていきます。
10.4.1 準備(Arrange)フェーズでのコードの再利用
まずは簡単にできることから始めていきましょう。つまり、最初は、元のコードがあったテスト・クラスと同じテスト・クラスの中にファクトリ・メソッドを配置するのです。そして・コードの重複が目立ってきたら、そのときに、ファクトリ・メソッドを個別のヘルパー・クラスに移すようにします。ただし、ファクトリ・メソッドを基底クラスに配置することは行わないようにしましょう。なぜなら、基底クラスには、すべてのテスト・メソッドで実行されるコード(たとえば、テストで使われたデータの後始末などを行うコード)だけを渡せるようにすべきだからです。
テストコード以外の普段の開発でも同じ考え方が大事だと思った。
10.4.2 実行(Act)フェーズでのコードの再利用
この実行フェーズもリファクタリングが可能です。たとえば、次のリスト10.11で示すように、テスト対象システム(今回の場合は、UserController)の機能への呼び出しが委譲されるメソッドを導入することが考えられます。
10.4.3 確認(Assert)フェーズでのコードの再利用
確認フェーズのリファクタリングも前述の準備(Arrange)フェーズと同じように、CreateUserメソッドやCreateCompanyメソッドのようなヘルパー・メソッドを導入することができます。
10.4.4 テストの際にデータベース・トランザクションの数が多くなる事は問題にはならないのか?
ここで考えなくてはならない事は、データベース・トランザクションの数が増える事は問題なのか、そして、もし、それが問題なのであれば、その問題に対して何ができるのか、ということです。
10.5.2 リポジトリをテストすべきか?
そのため、O/Rマッパーを使っているのであれば、リポジトリを直接検証するのではなく、統合テストのシナリオの一部に含めて検証するようにしましょう。 同様に、EventDispatcher クラス(ドメイン・イベントをもとに管理下にない依存への呼び出しを行うクラス)も個別にテストしないようにしましょう。なぜなら、このようなクラスをテストしても、複雑なモックの仕組みを維持するために膨大なコストがかかるのにもかかわらず、退行に対する保護はほとんど得られないからです。
第4部: 単体テストのアンチ・パターン
第11章: 単体テストのアンチ・パターン
11.1.3 プライベートなメソッドを直接テストすることが受け入れられるのはどのような場合なのか?
ただし、ここで注意して欲しいのは、プライベートなメソッドをテストすること自体は悪いことでは無い、ということです。プライベートなメソッドをテストすべきではない唯一の理由は、プライベートなメソッドは実装の詳細に繋がるものであるため、実装の詳細がテストされるようになると、そのテストは壊れやすいものになってしまうからです。とは言え、極めてまれに、プライベートでありながらも観察可能な振る舞いの一部となるメソッドが存在することがあります(そして、このことは、表11.1の「該当なし」は正確には正しくないことになります)。
また、これとは別の選択肢として、もし、このクラスのAPIをプライベートのままにしたいのであれば、テストの際にリフレクションを用いてInquiryクラスのインスタンスを生成することも可能です。これはある種の裏技のように思えるかもしれませんが、O/Rマッパーと同じことをしているだけに過ぎません。(O/Rマッパーも、内部ではリフレクションを用いてインスタンスを生成しています)。
11.2 プライベートな状態の公開
しかしながら、これはアンチパターンです。ここで思い出してほしいのが、テストでは、本番環境と全く同じ方法でテスト対象のコードとやり取りをしなくてはならない、ということです。つまり、テストだからと言って特別なことが許されるわけではないのです。
11.3 テストへのドメイン知識の漏洩
基本的に、このようなテストはプロダクション・コードをテストコードにコピー&ペーストしていると何ら変わりがないことになります。 結局のところ、このようなテストは実装の詳細と結びついた別の形の例に過ぎません。そのため、このようなテストはリファクタリングへの耐性をほぼ持っておらず、テストとしての価値がないことになります。
その代わり、次のリスト11.7で示すように、期待値そのものをテスト・コードに直接書き込みます。
なぜなら、期待値を直接書き込む事は、プロダクション・コードを使わずに別の方法でその結果を算出することを意味するからです。
11.4 プロダクションコードへの汚染
定義: プロダクション・コードへの汚染とは、テストでのみ必要とされるコードをプロダクション・コードに加えることを指します。
ただし、ここで注意してほしいのは、ILoggerインターフェイスの導入も間違いなくプロダクション・コードへの汚染である、ということです。なぜなら、このインターフェイスの導入はテストのためだけに行ったことだからです。それでは、このような実装にすることで何が以前より良くなるのでしょうか? まずILoggerインターフェイスの導入によって生じるプロダクション・コードへの汚染は、真偽値を用いた場合と比べて影響が少なく、その対処も簡単に行えます。さらに、真偽値を用いていた時とは異なり、テスト時にしか使われないはずのコードが間違って本番環境で呼び出されることもなくなります。加えて、インターフェースにはバグを含める事はできません。なぜなら、インターフェイスは単なる契約であり、具体的なコードを持つわけではないからです。このように、インターフェイスの導入は、真偽値を用いる場合とは異なり、処理に関するコードがそこに含まれるわけではないため、そのことが原因でバグが生じる事は無いのです。
11.5 具象クラスに対するテスト・ダブル
NOTE: 既存の機能をそのまま使えるようにするために、具象クラスをテスト・ダブルの対象にする事はアンチ・パターンです。もし、そうしなくてはならないのであれば、その具象クラスが単一責任の原則を遵守していないことが原因となっている可能性があります。
まとめ
現在日時を 環境コンテキスト(ambient context) として表現する事はプロダクション・コードを汚染することであり、テストの実施を難しくしてしまう。そのため、現在日時は明示的に依存として注入させなくてはならない。また、この注入にはサービスとして注入する方法と値として注入する方法の2つの方法があり、可能な限り、値として注入することを選択する。
おわりに
読み始めたのは2024年7月25日(木)。そして、読み終わったのは2024年10月7日(月)。75日間かかった。
一日に一回本をひらけば勝ち!1ページ読めば大勝利!という目標で読み進めてきました。
今回は大事だなぁと思ったり、ここは印象に残ったなぁと思った部分をボールペンで下線を引き、そこを引用して抜き出す。そしてコメントしたければする、ということを記録として残しました。
もっと整理したり、まとめたりした方がより良いのでしょうが、読書を継続する、ということを目標にしたかったので、妥協して手を抜いて、これでよしとしました。
後日、前この本で読んだこれなんだっけ?となり、このページを見て、ああこれだと本に辿り着くようなことが起これば、成功、と考えています。 ただ、、、このブログページを書いている今、とても長い分量になり、そしてそれはほぼ引用であることから、すでにもう、あまり読み返したくないな、と感じています。気軽にこのページを読み直すようにできたらと思いますが、何か改善できることはあるでしょうか?
とはいえ、しばらくこのやり方をベースにして、次の本、そしてまた次の本と、読書を途切れさせないで続けていこうかなと考えています。
以上です。