モックとスタブの違い スタイルによる違い

スタイルによる違い

どちらのスタイルも有利な点と付随的に処理することがある。どちらのスタイルを使えばいいのかを理解するには、考慮することがかなりある。

フィクスチャのセットアップ

状態中心のテストでは、メッセージに対する応答に関わる全てのオブジェクトを生成しないといけない。上の例では2つのオブジェクトしかなかったが、実際のテストでは多数のサブオブジェクトが関係してくることが多い。通常はテストが実行される度に生成され解放される。

しかし相互作用中心のテストでは、メインのオブジェクトと差し当たり必要なモックだけを生成すればいい。これは幾つもの必要なオブジェクトを揃える際の、手のかかる作業を多少回避してくれる。

実際には、状態ドリブンのテストをする人々は必要なオブジェクトを揃える部分をできるだけ再利用するように気を付けている。再利用を行うための一番簡単な方法は、オブジェクトのセットアップコードをxUnitのセットアップメソッドに入れることだ。幾つものテストで使われるより複雑なオブジェクトがある場合は、特別なオブジェクト生成クラスを作る。私はこのクラスを"オブジェクトマザー"*1と呼んでいる。これは初期のThoughtWorksのXPプロジェクトで使われた命名法によっている。大規模な状態中心テストではオブジェクトマザーは必要不可欠だが、オブジェクトマザーはメンテナンスが必要な付加的コードであり、どんな変更でもテスト全体に重要な影響を波及させる。また、フィクスチャのセットアップにかかるパフォーマンス上のコストのこともある。ただこれについては、適切に行っていれば、深刻な問題になったと聞いたことはない。ほとんどのフィクスチャオブジェクトは生成にコストがかからないし、コストのかかるものは普通スタブ化される。

結局私はどちらのスタイルも相手の方がやることが多いと言うのを聞いてきた。相互作用スタイルのテストを行う人はフィクスチャを作るのはとても手間がかかると言うし、状態スタイルのテストを行う人は、フィクスチャは使い回される、だがモックは全てのテスト毎に作らないといけない、と言う。

テストドリブン開発

モックオブジェクトはXPコミュニティから出てきたものだ。また、XPの主な特徴の一つは、テストドリブン開発を重要視していることだ。テストドリブン開発では、テストを書くことで推進される反復を通じてシステム設計も進んでいく。

そういったわけで、モックオブジェクトの支持者達が相互作用テストの設計における効果についてことさら語るのも驚くことではない。このスタイルでは、主要なオブジェクトに対する最初のテストを書くから [それらのオブジェクトの] 振る舞いの開発を始めることになる。サブオブジェクトについての想定を考えることで、メインオブジェクトとそれに関係するオブジェクトの間の相互作用を検証することになる。効果的なにメインオブジェクトの外向けのインターフェースをデザインしながらだ。いったん最初のテストを実行したら、後はモックに対する想定が、テストの次のステップと、それをどこからやればいいかを明確な指示を与えてくれる。

特に、このやり方は機能をアウト-インで実装していくように促す。まず下位のモックレイヤーを使ってユーザーインタフェースのプログラミングを始め、それから、下位のレイヤーについてテストを作りつつ、システムの一つのレイヤーを少しずつ [作り上げて] 進んで行く。

状態中心のテストはこれと同じようなガイダンスは与えてくれない。状態中心のテストを行う人達はしばしば関係するオブジェクトの集まりをまとめて手を打とうとする。少しずつ進んで行く同じようなやり方はできるのだ。まずフェイクメソッドを持ったサブオブジェクトを作り、それから実際のメソッドと置き換え、そしてそのサブオブジェクトのテストをすればいい。特にレイヤー単位で作業する、ということについていえば、状態中心のテストでは、まずドメインロジックを実装してからユーザーインターフェースを作るというミドル-アウトのスタイルがより一般的のようだ。

テストの分離

もし相互作用テストでシステムにバグを取り込んでしまっても、バグを含んだオブジェクトをメインオブジェクトとするテストだけが失敗することになる。しかし状態中心の方法では、バグオブジェクトをサブオブジェクトとして使うオブジェクトのテストも全て失敗することになる。つまり多用されるオブジェクトの一つの不具合がシステム全体のテストの失敗へと波及することになる。

このため、エラーの根本的な原因を見つけて直すのに、数多くのデバッグをおこなう結果になる。相互作用スタイルのテストを行う人々はこれを状態中心テストの重大な問題だと考える。しかし状態中心スタイルのテストを行う人々はこれを問題の根源とは言わない。大抵の場合どのテストが失敗するかを見れば悪いところは比較的簡単に見つけ出せるし、開発者なら他の不具合が根本の原因から起きていることがわかる。

テストの粒度はここで重要になることの一つだろう。状態中心のテストは複数のオブジェクトを用いるため、一つのテストでも、一つのオブジェクトに対してというより、いくつかのオブジェクトの集まりに対してのテストであるのが、状態中心のではメインであるのをよく見かける。もしその集まりが多くのオブジェクトにわたるものだとしたら、バグの本当の原因を見つけるのはずっと困難になることもある。実はこういう場合に起きているのは、テストの粒度が粗くなっていることなのだ。

相互作用中心のテストでこの問題に頭を悩ますことはあまりなさそうというのは、実際にありそうなことだ。なぜなら、メインオブジェクトを欺くように全てのオブジェクト [に対するモックオブジェクトなど] を作るのが相互作用中心のテストであり、そのことでちょうどいい粒度のテストがサブオブジェクトには必要だというのが明らかになるからだ。とは言うものの、あまりに粗いテストは必ずしもテクニックとしての状態中心テストの欠点ではなく、むしろ状態中心テストをきちんと行う際の失敗であるというのもまた事実だ。経験から言えるのは、それぞれのクラスに対して適度な粒度でテストを切り分けるように講じるのがいいとういことだ。幾つかのオブジェクトをまとめてテストすることも時には理に適うのだが、その時は6個未満の数個のオブジェクトに限られるべきだろう。さらに、粗すぎる粒度のテストのためにデバッグで問題があった時には、テストドリブンの方法でデバッグしていくべきで、そうしていれば、ちょうどいい粒度のテストが作られていくだろう。

基本的に、状態中心テストは単にユニットテストであるだけでなく、ミニ結合テストでもある。そのため多くの人達が、あるオブジェクトに対するテストが見逃すであろうエラーをクライアントテスト、特にクラスの相互作用が起きる箇所の精査によってキャッチすることもできるという事実を好む。[いろいろなパスを実行しながら網羅する方法に比べて] 相互作用テストはこの [相互作用のチェック] の質を下げる。その上、ユニットテストでグリーン [xUnit系のツールでエラーがなかったことを意味する色] になっても潜在的なエラーを見逃しているというリスクを冒すことにもなる。

現時点で私が強調しておきたいのは、どちらのテストスタイルを用いるにしても、システム全体を縦断して操作する受け入れテストも一緒に少し粗い粒度で行うべきだ、ということだ。

テストと実装との連動

相互作用テストを書く時には、メインオブジェクトが自身を使うオブジェクトに対して正しく反応するのを確かめるために、メインオブジェクトの外部呼び出しをテストしていることになる。状態中心のテストでは、どのように状態が導かれたかではないく、最終状態だけに関心が集められる。だから相互作用中心のテストの方がよりメソッドの実装に結びついている。サブオブジェクトの呼び出し方が変われば、状態中心テストが中断を起こすようになるだろう。

この連動によって2つ考えることが浮かぶ。一番重要なのはテストドリブン開発における効果だ。相互作用中心のテストでは、テストを書くことが振る舞いの結びつきを考えさせることになる。確かに相互作用テストをする人はこのことを利点と見ている。しかし状態中心のテストをする人は、外部インターフェースから何が起きるかのみ考えて、実装について考えるのはテストを書き終えるまで全て後回しにするのが重要だと考える。

実装の変更はテストを壊しやすい。だから、実装との結びつきは、状態中心のテストの妨げになる以上にリファクタリングの妨げにもなる。

相互作用中心のツールキットの性質のため、これがさらに悪化することもある。モックツールは大抵決められたメソッドの呼び出し方やパラメータの一致を設定する。呼び出しやパラメータがあるテストでは関係なくても、だ。jMockツールキットの目的の一つは、想定が影響ないところでは想定の設定を緩くできるようなフレキシブルさを持つことなのだ。

設計スタイル

この2つのテストスタイルで私が一番魅了される面は、どれくらい設計の決定に影響を及ぼすかということだ。以前私はスタイルが推奨するデザインの幾つかの違いに気付いた、と両方のテストスタイルの人に話したことがあるのだが、今では私はほんの表面を引っかいているにすぎないと確信している。

すでに [テストドリブン開発の項で] レイヤーへの取り組みでの違いのひとつについて述べた。相互作用中心のテストは、インタフェース作成 [原文:presentation(実演/提示)] から始める方法によって、外部をサポートする。メインモデルから離れたスタイルを好む開発者は状態中心テストを好む傾向にある。

もう少し小さなレベルでは、相互作用中心のテストをする人は、値を返すメソッドを少なくし、収集オブジェクトに作用するメソッドを支持する傾向がある。
例として、インフォメーション文字列を生成するのに、オブジェクト郡から情報を集める行いを挙げよう。通常やることは、各オブジェクトの文字列を返すメソッドを呼び出して、その結果の文字列を一時変数の中で [レポート文字列に] 組み上げるレポートメソッドを作ることだ。相互作用テストを行う人は、各オブジェクトにストリングバッファ [=文字列を格納するスペース] を渡し、各オブジェクトがそのバッファへそれぞれの文字列を追加するようにする。こうする時、ストリングバッファは収集パラメータとして扱われることになる [このパラメータの値がインフォメーション文字列となる]。

相互作用中心テストを行う人々は”列車事故”を回避することについて語りたがる。”列車事故”とはgetThis().getThat().getTheOther()のようなスタイルのメソッド連鎖のことだ。メソッド連鎖をしないようにすることは、デメテルの法則*2に従うこととして知られている。メソッドの連鎖の臭いがあるかぎり、フォワーディング [=転送] ・メソッドによって膨れあがった仲介オブジェクトの問題の臭いもすることになる。(デメテルの法則はデメテルの提案と呼ばれるほうがしっくりくるのじゃないかと私はいつも思うのだが)

人々にとってオブジェクト指向設計を理解するのが最も難しいことの一つが ”Tell, Don't Ask(告げろ、尋ねるな)” 原則 だ。この原則は、なにかをしようとするコードで、そのためにあるオブジェクトからデータをもぎ取ってくるのではなく、それをするようにあるオブジェクトに告げることを薦める。相互作用テストをする人は、相互作用テストがこの原則を促進することを手助けし、最近のコードにあまりにはびこっている getter紙吹雪 [=getterメソッドの多用] を避けるようにしてくれる。

*1:http://www.xpuniverse.com/2001/pdfs/Testing03.pdf

*2:オブジェクト指向の法則の一つ。他のオブジェクトから取得したオブジェクトへのアクセスをしないことで、クラス間の密結合を抑えようとする。