スタブ・モックは本当に悪者なのか?〜テスト駆動開発をやめて、なお残すべき習慣とは (2)

TDD批判の際に真っ先に矛先を向けられるスタブ・モックは本当に役に立たない代物なのでしょうか?自身の経験を踏まえて再考してみます。

Eiji Ienaga
時を超えたプログラミングの道

--

ぼくはわるものなの?

はじめに

TDDやユニットテストについての批判については、スタブ・モックが、いくつかの記事にでも批判対象に上がります。

“実際にアジャイルやTDDの実践者の話を思い返しても、彼らがDHH氏が言うところの厳密な意味でのユニットテスト(依存関係は全てMockにして、対象となるユニットだけをテスト対象にする)にこだわっている印象はない。むしろ、DHH氏と同様、過剰なMockの導入は無駄が多いし、本来テストすべき箇所をテスト出来ないという問題意識を持っている人が多いように思う。”

かくいう私も頃の10年ほど前の2つ目の開発プロジェクトではスタブ・モックを使ったテストのメンテナンスに痛い目に合いました。

けれども、この痛い経験をしたからといって、スタブ・モックを以降のプロジェクトで全く使わなくなったということはありません。

今日は、スタブ・モックを軽くおさらいし、設計判断のヒントとなるヘキサゴナルアーキテクチャとその系統を抑えて、スタブ・モックの使い所を改めて再検討します。

スタブ・モックって何?

スタブのおさらい

テスト対象オブジェクト(SUT)が若干ややこしい隣人オブジェクト(DOC)とコラボレートしている前提で、隣人オブジェクトからの間接入力(Indirect Input)によって実行結果が決まる場合、どのようにSUTが期待通り動作したかを検証すればよいでしょうか?その時 Test Stubの出番です。Test Stubで予め間接入力(Indirect Input)させたい値をテストコードで明記し、DOCと差し替え、SUTが期待通り動作するかを確認します。DOCでの発生条件が難しい値や例外系であっても、Test Stubであればテストコードからコントロールしやすく、SUTの動作確認が簡単に行なえます。

http://xunitpatterns.com/Test%20Stub.html

モックのおさらい

テスト対象(SUT)がややこしい隣人オブジェクト(DOC)とコラボレートしている前提で、どうやってSUTからDOCへと期待通りメッセージングしたかを確認すればよいでしょうか?このようなときにMock Objectの出番です。Mock Objectに予め間接出力(Indirect Output)の期待する振る舞いをテストコードで明記し、DOCと差し替え、SUTが期待通りお隣さんとコラボレートしたかを検証(Verify)します。

http://xunitpatterns.com/Mock%20Object.html

イマドキのモック のライブラリはオブジェクト指向の重要概念であるオブジェクト間のメッセージパッシング(≒SUT-DOC間の間接出力、間接出力)の振る舞いを簡潔明瞭にDSLで記述できるように工夫されています。

スタブ・モックについては次も参照して下さい。

では、動作確認する際に、テスト対象から切り離したいややこしい隣人(DOC)は一体なにに相当するのでしょうか。次に紹介するヘキサゴナルが1つ判断材料になります。

ヘキサゴナルアーキテクチャ、クリーンアーキテクチャ

ソフトウェアの設計を大枠で考るとき、上下のメタファで整理するレイヤーアーキテクチャのほか、中央と周辺のメタファーで整理する方法があります。中央と周辺で整理は、アリスターコバーンのヘキサゴナルアーキテクチャが有名です。その後、実践テスト駆動開発The Clean Architectureの中で変形されて引用されいます。スタブ・モックの使い所を検討するのに、この中央と周辺による整理は便利です。ドメイン駆動設計の場合は、ドメインを中心に据えることになります。

http://alistair.cockburn.us/Hexagonal+architecture
http://www.natpryce.com/articles/000772.html
https://8thlight.com/blog/uncle-bob/2012/08/13/the-clean-architecture.html

技術詳細は外側へ寄せる

ポイントは、中心となる対象ドメインは何か?中心から排除したい要素は何か?を考慮して区分けすることです。中心のドメインから排除したい項目の代表例が、データベースアクセスや外部Webシステムやメッセージングといった詳細の技術要素です。ドメイン駆動設計の設計判断を取り入れている場合は、オブジェクトへのアクセスするためのRepositoryのインタフェースのみを中心ドメインに入れ込み、Repositoryの実装(特定のデータベース種類やSQLなど実装詳細)は外側に追いやります。同様に、インタフェースのみを中心にいれてメッセージングや他のWebシステムのアクセス等の実装の詳細は外部に追いやります。

うまく区分けできれば、中央に残った純粋なビジネスについてのルールや状態遷移についてをユニットテストやリファクタリングを継続することができます。リファクタリングによる設計改善を継続することで、対象ドメインについて理解を深めることができます。この区分けの箇所でスタブやモックの出番がでてきます。

レガシーシステムの詳細は外側へ寄せる

開発対象の中心ドメインの外側に追いやりたいのは技術要素に限りません。連携予定の古い他システムのドメインの語彙体系も含まれます。 古いシステムのドメインの語彙体系と開発対象のドメインの語彙体系が混ざると混乱します。同音異義語のクラスやメソッドやプロパティが混ざり込むと目も当てられません。そのような時は2つのドメインの変換レイヤーを設け、開発対象の中心ドメインが、レガシーシステムの語彙体系で汚染されないよう外側に追いやります。この区分けの箇所もスタブやモックの出番が考えられます。腐敗防止層については書籍ドメイン駆動設計や下記を参照にして下さい。

https://docs.microsoft.com/en-us/azure/architecture/patterns/anti-corruption-layer

https://www.ogis-ri.co.jp/otc/hiroba/technical/DDDEssence/chap3.html#AnticorruptionLayer

このほか、ビジネスドメインの中でも、ビジネスルールの仕様と複雑なアルゴリズムの実現手段を分離したくなるかもしれません。

ドメイン/周辺環境の複雑さによってモックスタブの使い所ポイントは変わる!

スタブ・モックの使い所の判断ポイントが幾つかあります。たとえば、「中央のドメインは複雑か?」「周囲の環境は多様か、準備コストは高価か?」「繰り返しテストは可能か」「テストを利用して理解を深めたいことはなにか?」などです。

A案:中央と周辺の分離を消極的に行う作戦

もし、開発対象が「中央のドメインは複雑ではない。大したビジネスルールも、状態遷移もない。想定アクターも1つ。」「周辺の技術要素の登場要素は少ない。変える予定がない。」「周辺環境の準備は簡単・安価で、ツールも揃っておりAll Test Greenを保つ見込みがある。」「結合テストの総件数は小さく、繰り返し実行コストも小さい(件数あるがテストの並列化が可能)。」なのであれば、ヘキサゴナルアーキテクチャ系統の設計判断の必要性は低くなっていきます。

中心ドメインは複雑ではなく周辺環境も複雑ではない、結合テスト件数も限られるのであれば、モックスタブの出番が減り、トランザクションスクリプトでサラッとつくってしまい、UI or RESTの入り口の外側からデーターベースまで結合して機能性が継続して担保できているかに注力する動機のほうが強くなるでしょう。もしかしたらテスト駆動でわざわざSTEP BY STEPで試行錯誤して、理解しやすい&修正しやすい&テストしやすい構造と振る舞いを発見しながらつくる作戦よりも、LoopBackのようなフレームワークの流儀に則って、殆どプログラミングしないでREST出入り口からデータベースまで動作するものをさらりとつくってしまう作戦の方がベターかもしれません。

疎にしたい周辺要素Aの登場に注意!

ただし、トランザクションスクリプトの作戦でデータベースと密な関係のつくりは許容しても、外部システムに依存する場合は疎な関係で作りたくなる動機が生まれます。この部分に関しては、スタブ・モックの出番がでてきます。

視点を中央から周辺に移す

中央ドメインの外側に配置された他システムやAndroidやPolymerやReactといったフロント部分に視点を移すとどうでしょうか。バックエンド部分と独立して作業をすすめたいの動機が生まれます。この部分も、スタブ・モックの出番の余地があります。

境界の切り方として、ドメイン駆動設計のRepositoryのようなドメイン寄りのインタフェースを用意しない場合も、RESTのインタフェースをフロントとバックの境界線とみなし、ライブラリーやフレームワークが提供するテスト支援ツールの流儀に則って(例えば、TestRestTemplate, webmock)差し替える作戦をとることになるでしょう。

B案:中央と周辺の分離を積極的に行う作戦

一方、対象のビジネスドメインが複雑かつ周辺環境も多様で複雑な場合はどうでしょうか。

中央ドメインも複雑で周辺も複雑なのであれば、複雑な周辺環境とは分けて複雑な中央ドメインをターゲットに、ドメインに対する洞察を深めるためのモデリング・テスト・リファクタリングを行いたい動機が生まれます。ユニットテストやスタブ/モックの出番が増えてきます。

例えば、オフィス複合機で動くアプリケーション開発の場合を考えてみましょう。End2Endで機能性を確認するには、複合機実機のほかにネットワーク通信やファイルシステム等も必要になってきます。複合機実機自体の製品バリエーションも多数あります。が、 全テストパターンをすべての環境込みで動作確認するとなると、準備から手間がかかりすぎます。複合機が別チームが作成中の外部サービスと連携が必要なのであれば、調整コストもかかるかもしれません。複合機アプリの中心ドメインに注力してテストとリファクタリングの試行錯誤できる状況を作りたくなるでしょう。

スタブ・モックが「あなた自身を騙す」

すべての技法には、メリットデメリットがあります。ユニットレベルのテストのみに熱中すると盲点が生まれがちです。開発者ならユニットテスト対象のオブジェクト(SUT)の周辺の前提が間違ったスタブ・モックを一度は作ったことあるのではないでしょうか?

ユニットテストでは環境込みで結合して動作を担保し続けることは保証してくれません。ユニットテストは、負荷テスト、探索的テスト、ユーザビリティテストなどなどの代わりにはなりません。スタートアップの初期に知りたい、プロダクトが市場にフィットするかの判断の情報はユニットテストでは提供してくれません。プロダクトコードについて私達が知らないことがいったい何で、何をすれば知識を獲得できるのかを自分たちで考えて、実験を繰り返して行く必要があります。

まとめ

スタブとモックのおさらいをして、ヘキサゴナルアーキテクチャを紹介し、スタブ・モックの使い所を再検討しました。

中心ドメインが複雑で周辺環境も複雑であれば、中央と周辺の環境を切って中央の洞察が深められるようにします。中央も周辺も複雑でなく必要ないと判断するならトランザクションスクリプトでビジネスロジックと周辺環境(例えばデータベース)を一体に書いて纏めてテストする作戦のほうが手っ取り早いでしょう。後者の場合はモックやスタブの出番は少なくなります。

ただし、スタブ・モックを使ったユニットテストに熱中するあまり外側の座視が疎かになると、外側の環境との入出力パターンが抜け落ちたり、機能性で達成したいことが何かを誤解して作ってしまったりなりがちです。並行して別のテストも合わせて行ないたいところです。

次回は、落とし穴に陥らないように、実践テスト駆動開発実践アジャイルテストを参考に多重の学習フィードバックループによる知識獲得の仕組みについて解説します。

--

--