モックはスタブではない
「モックオブジェクト」という用語は、テストのために実際のオブジェクトを模倣する特殊なケースのオブジェクトを記述するために、よく使われるようになってきました。 ほとんどの言語環境には、モックオブジェクトを簡単に作成できるフレームワークが用意されています。 しかし、モックオブジェクトは特殊なケースのテストオブジェクトの一種であり、異なるスタイルのテストを可能にするものであることが、しばしば認識されていません。 この記事では、モックオブジェクトの仕組み、モックオブジェクトが動作検証に基づくテストをどのように促進するか、そしてモックオブジェクトを中心としたコミュニティが、それらを使用してどのように異なるスタイルのテストを開発しているかを説明します。
2007年1月2日
私は数年前にエクストリームプログラミング(XP)コミュニティで「モックオブジェクト」という用語に出会いました。それ以来、私はモックオブジェクトにますます遭遇するようになりました。これは partly ソートワークスで私の同僚であったモックオブジェクトの主要な開発者が何人もいたためです。 partly XPに影響を受けたテストに関する文献で、それらをますます見かけるようになったからです。
しかし、モックオブジェクトが不適切に記述されているのをよく見かけます。特に、テスト環境の一般的なヘルパーであるスタブと混同されていることがよくあります。私はこの混乱を理解しています。私も以前はそれらを似たようなものと見ていましたが、モック開発者との会話を通して、少しずつモックの理解が私の頭蓋に浸透してきました。
この違いは、実際には2つの別々の違いです。 一方では、テスト結果の検証方法の違い、つまり状態検証と動作検証の違いがあります。 他方では、テストと設計がどのように連携するかについての全く異なる哲学があり、ここではテスト駆動開発の古典的スタイルとモック主義スタイルと呼んでいます。
通常のテスト
まず、簡単な例を使って2つのスタイルを説明します。(例はJavaですが、原則はどのオブジェクト指向言語でも理解できます。)注文オブジェクトを取得し、倉庫オブジェクトからそれを満たしたいと考えています。注文は非常にシンプルで、1つの製品と数量のみです。倉庫はさまざまな製品の在庫を保有しています。注文に倉庫から在庫を補充するように依頼すると、2つの可能な応答があります。倉庫に注文を満たすのに十分な製品がある場合、注文は満たされ、倉庫の製品の量は適切な量だけ減少します。倉庫に十分な製品がない場合、注文は満たされず、倉庫では何も起こりません。
これらの2つの動作は、いくつかのテストを意味します。これらはかなり従来のJUnitテストのように見えます。
public class OrderStateTester extends TestCase { private static String TALISKER = "Talisker"; private static String HIGHLAND_PARK = "Highland Park"; private Warehouse warehouse = new WarehouseImpl(); protected void setUp() throws Exception { warehouse.add(TALISKER, 50); warehouse.add(HIGHLAND_PARK, 25); } public void testOrderIsFilledIfEnoughInWarehouse() { Order order = new Order(TALISKER, 50); order.fill(warehouse); assertTrue(order.isFilled()); assertEquals(0, warehouse.getInventory(TALISKER)); } public void testOrderDoesNotRemoveIfNotEnough() { Order order = new Order(TALISKER, 51); order.fill(warehouse); assertFalse(order.isFilled()); assertEquals(50, warehouse.getInventory(TALISKER)); }
xUnitテストは、典型的な4つのフェーズシーケンス(セットアップ、実行、検証、ティアダウン)に従います。この場合、セットアップフェーズは partly setUpメソッド(倉庫のセットアップ)で、partly テストメソッド(注文のセットアップ)で行われます。 order.fill
の呼び出しは実行フェーズです。これは、テスト対象のオブジェクトに動作を実行させる段階です。assertステートメントは検証段階であり、実行されたメソッドがタスクを正しく実行したかどうかを確認します。この場合、明示的なティアダウンフェーズはなく、ガベージコレクタが暗黙的にこれを行います。
セットアップ中に、2種類のオブジェクトを組み合わせています。Orderはテスト対象のクラスですが、Order.fill
が機能するためには、Warehouseのインスタンスも必要です。この状況では、Orderはテストに焦点を当てているオブジェクトです。テスト指向の人々は、このようなものを指名するために、テスト対象オブジェクトやテスト対象システムなどの用語を使用するのが好きです。どちらの用語も言うには厄介ですが、広く受け入れられている用語なので、我慢して使用します。Meszarosに従って、テスト対象システム、つまり略語SUTを使用します。
そのため、このテストにはSUT(Order
)と1つのコラボレーター(warehouse
)が必要です。倉庫が必要なのは2つの理由からです。1つはテスト対象の動作を überhaupt 機能させるため(Order.fill
は倉庫のメソッドを呼び出すため)、2つ目は検証のためです(Order.fill
の結果の1つは倉庫の状態の潜在的な変化であるため)。このトピックをさらに詳しく見ていくと、SUTとコラボレーターを区別することがたくさんあることがわかります。(この記事の以前のバージョンでは、SUTを「プライマリオブジェクト」、コラボレーターを「セカンダリオブジェクト」と呼んでいました。)
このテストスタイルでは、**状態検証**を使用します。つまり、メソッドが実行された後にSUTとそのコラボレーターの状態を調べて、実行されたメソッドが正しく機能したかどうかを判断します。後で見るように、モックオブジェクトは検証に異なるアプローチを可能にします。
モックオブジェクトを使用したテスト
それでは、同じ動作を使用してモックオブジェクトを使用します。このコードでは、モックを定義するためにjMockライブラリを使用しています。jMockはJavaモックオブジェクトライブラリです。他にもモックオブジェクトライブラリはありますが、これはこの手法の創始者によって書かれた最新のライブラリであるため、始めるのに適しています。
public class OrderInteractionTester extends MockObjectTestCase { private static String TALISKER = "Talisker"; public void testFillingRemovesInventoryIfInStock() { //setup - data Order order = new Order(TALISKER, 50); Mock warehouseMock = new Mock(Warehouse.class); //setup - expectations warehouseMock.expects(once()).method("hasInventory") .with(eq(TALISKER),eq(50)) .will(returnValue(true)); warehouseMock.expects(once()).method("remove") .with(eq(TALISKER), eq(50)) .after("hasInventory"); //exercise order.fill((Warehouse) warehouseMock.proxy()); //verify warehouseMock.verify(); assertTrue(order.isFilled()); } public void testFillingDoesNotRemoveIfNotEnoughInStock() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); warehouse.expects(once()).method("hasInventory") .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); assertFalse(order.isFilled()); }
まずtestFillingRemovesInventoryIfInStock
に集中してください。後のテストではいくつかのショートカットを使用しているためです。
まず、セットアップフェーズが非常に異なります。まず、データと期待の2つの部分に分かれています。データ部分は、作業に関心のあるオブジェクトを設定します。その意味では、従来のセットアップに似ています。違いは、作成されるオブジェクトにあります。SUTは同じです - 注文です。ただし、コラボレーターは倉庫オブジェクトではなく、モック倉庫です。技術的にはMock
クラスのインスタンスです。
セットアップの2番目の部分は、モックオブジェクトに対する期待を作成します。期待は、SUTが実行されたときにモックでどのメソッドが呼び出されるかを示します。
すべての期待が設定されたら、SUTを実行します。実行後、検証を行います。検証には2つの側面があります。以前と同様に、SUTに対してassertを実行します。ただし、モックも検証します。期待どおりに呼び出されたかどうかを確認します。
ここでの重要な違いは、注文が倉庫とのやり取りで正しいことをしたかどうかを検証する方法です。状態検証では、倉庫の状態に対してassertを実行することでこれを行います。モックは**動作検証**を使用します。ここでは、注文が倉庫に対して正しい呼び出しを行ったかどうかを確認します。このチェックは、セットアップ中にモックに何を期待するかを伝え、検証中にモックに自己検証を依頼することで行います。assertを使用してチェックされるのは注文のみであり、メソッドが注文の状態を変更しない場合は、assertは全くありません。
2番目のテストでは、いくつかの異なることを行います。まず、コンストラクタではなくMockObjectTestCaseのmock
メソッドを使用して、モックを異なる方法で作成します。これはjMockライブラリの便利なメソッドであり、後でverifyを明示的に呼び出す必要がないことを意味します。便利なメソッドで作成されたモックは、テストの最後に自動的に検証されます。最初のテストでもこれを行うことができましたが、モックを使ったテストの仕組みを示すために、検証をより明示的に示したかったのです。
2番目のテストケースの2番目の違いは、withAnyArguments
を使用して期待の制約を緩和したことです。これは、最初のテストで倉庫に番号が渡されることをチェックするため、2番目のテストではテストのその要素を繰り返す必要がないためです。注文のロジックを後で変更する必要がある場合、1つのテストのみが失敗するため、テストの移行作業が軽減されます。結局のところ、withAnyArguments
はデフォルトであるため、完全に省略することもできました。
EasyMockの使用
モックオブジェクトライブラリはたくさんあります。私がよく目にするのはEasyMockで、Java版と.NET版の両方があります。EasyMockも動作検証を可能にしますが、jMockとはスタイルにいくつかの違いがあり、議論する価値があります。おなじみのテストをもう一度示します。
public class OrderEasyTester extends TestCase { private static String TALISKER = "Talisker"; private MockControl warehouseControl; private Warehouse warehouseMock; public void setUp() { warehouseControl = MockControl.createControl(Warehouse.class); warehouseMock = (Warehouse) warehouseControl.getMock(); } public void testFillingRemovesInventoryIfInStock() { //setup - data Order order = new Order(TALISKER, 50); //setup - expectations warehouseMock.hasInventory(TALISKER, 50); warehouseControl.setReturnValue(true); warehouseMock.remove(TALISKER, 50); warehouseControl.replay(); //exercise order.fill(warehouseMock); //verify warehouseControl.verify(); assertTrue(order.isFilled()); } public void testFillingDoesNotRemoveIfNotEnoughInStock() { Order order = new Order(TALISKER, 51); warehouseMock.hasInventory(TALISKER, 51); warehouseControl.setReturnValue(false); warehouseControl.replay(); order.fill((Warehouse) warehouseMock); assertFalse(order.isFilled()); warehouseControl.verify(); } }
EasyMockは、期待の設定に記録/再生のメタファーを使用します。モックするオブジェクトごとに、コントロールオブジェクトとモックオブジェクトを作成します。モックはセカンダリオブジェクトのインターフェースを満たし、コントロールは追加機能を提供します。期待を示すには、モックで期待する引数を使用してメソッドを呼び出します。戻り値が必要な場合は、 इसके बाद コントロールを呼び出します。期待の設定が完了したら、コントロールで再生を呼び出します。この時点で、モックは記録を終了し、プライマリオブジェクトに応答する準備が整います。完了したら、コントロールで検証を呼び出します。
人々は、記録/再生のメタファーを一見すると戸惑うことが多いようですが、すぐに慣れるようです。jMockの制約よりも優れている点は、文字列でメソッド名を指定するのではなく、モックに対して実際にメソッド呼び出しを行うことです。これは、IDEでコード補完を使用できることを意味し、メソッド名の変更はテストを自動的に更新します。欠点は、より緩い制約を持つことができないことです。
jMockの開発者は、他の手法を使用して実際のメソッド呼び出しを使用できるようにする新しいバージョンに取り組んでいます。
モックとスタブの違い
モックオブジェクトが初めて導入されたとき、多くの人はスタブを使用するという一般的なテストの概念とモックオブジェクトを混同していました。それ以来、人々は違いをよりよく理解しているようです(そして、この論文の以前のバージョンが役立ったことを願っています)。しかし、人々がモックを使用する方法を完全に理解するには、モックと他の種類のテストダブルを理解することが重要です。(「ダブル」?これがあなたにとって新しい用語であっても心配しないでください。数段落待てばすべて明らかになります。)
このようなテストを行う場合、ソフトウェアの1つの要素に一度に焦点を当てています。そのため、単体テストという用語が一般的に使用されます。問題は、単一のユニットを動作させるために、多くの場合、他のユニットが必要になることです。そのため、この例では、ある種の倉庫が必要になります。
上記に示した2つのテストスタイルでは、最初のケースは実際の倉庫オブジェクトを使用し、2番目のケースはモック倉庫を使用します。これはもちろん、実際の倉庫オブジェクトではありません。モックを使用することは、テストで実際の倉庫を使用しない1つの方法ですが、このようなテストで使用される非現実的なオブジェクトには他にも種類があります。
これについて話すための語彙はすぐに混乱します。スタブ、モック、フェイク、ダミーなど、あらゆる種類の言葉が使用されます。この記事では、Gerard Meszarosの本の語彙に従います。誰もが使用するわけではありませんが、良い語彙だと思います。これは私のエッセイなので、どの単語を使用するかを選択できます。
Meszarosは、テスト目的で実際のオブジェクトの代わりに使用されるあらゆる種類の偽のオブジェクトの総称として、**テストダブル**という用語を使用しています。この名前は、映画のスタントダブルの概念に由来しています。(彼の目的の1つは、すでに広く使用されている名前の使用を避けることでした。)Meszarosは、5種類のダブルを定義しました。
- **ダミー**オブジェクトは渡されますが、実際には使用されません。通常、それらはパラメータリストを埋めるためにのみ使用されます。
- **フェイク**オブジェクトは実際に動作する実装を持っていますが、通常、本番環境には適さないショートカットを使用します(インメモリデータベースが良い例です)。
- **スタブ**は、テスト中に実行された呼び出しに対して事前に用意された回答を提供します。通常、テスト用にプログラムされていないものにはまったく応答しません。
- **スパイ**は、呼び出された方法に基づいていくつかの情報を記録するスタブです。この形式の1つは、送信されたメッセージの数を記録する電子メールサービスです。
- **モック**は、ここで説明しているものです。受信することが予想される呼び出しの仕様を形成する期待値で事前にプログラムされたオブジェクトです。
これらの種類のダブルの中で、モックだけが動作検証を主張します。他のダブルは、通常、状態検証を使用できます。モックは、実行フェーズ中は他のダブルのように動作します。SUTに実際の共同作業者と話をしていると信じさせる必要があるためです。しかし、モックはセットアップフェーズと検証フェーズで異なります。
テストダブルをもう少し詳しく調べるには、例を拡張する必要があります。多くの人は、実際のオブジェクトの操作が難しい場合にのみテストダブルを使用します。テストダブルのより一般的なケースは、注文の処理に失敗した場合に電子メールメッセージを送信したいと言った場合です。問題は、テスト中に顧客に実際の電子メールメッセージを送信したくないことです。そのため、代わりに、制御および操作できる電子メールシステムのテストダブルを作成します。
ここで、モックとスタブの違いがわかり始めます。このメール動作のテストを作成する場合、次のような単純なスタブを作成できます。
public interface MailService { public void send (Message msg); }
public class MailServiceStub implements MailService { private List<Message> messages = new ArrayList<Message>(); public void send (Message msg) { messages.add(msg); } public int numberSent() { return messages.size(); } }
次に、次のようにスタブで状態検証を使用できます。
class OrderStateTester...
public void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); MailServiceStub mailer = new MailServiceStub(); order.setMailer(mailer); order.fill(warehouse); assertEquals(1, mailer.numberSent()); }
もちろん、これは非常に単純なテストです。メッセージが送信されたことだけです。正しい人に送信されたか、正しい内容で送信されたかはテストしていませんが、要点を説明するにはこれで十分です。
モックを使用すると、このテストはまったく異なります。
class OrderInteractionTester...
public void testOrderSendsMailIfUnfilled() { Order order = new Order(TALISKER, 51); Mock warehouse = mock(Warehouse.class); Mock mailer = mock(MailService.class); order.setMailer((MailService) mailer.proxy()); mailer.expects(once()).method("send"); warehouse.expects(once()).method("hasInventory") .withAnyArguments() .will(returnValue(false)); order.fill((Warehouse) warehouse.proxy()); } }
どちらの場合も、実際のメールサービスの代わりにテストダブルを使用しています。スタブは状態検証を使用し、モックは動作検証を使用するという違いがあります。
スタブで状態検証を使用するには、検証に役立つスタブにいくつかの追加メソッドを作成する必要があります。その結果、スタブは `MailService` を実装しますが、追加のテストメソッドを追加します。
モックオブジェクトは常に動作検証を使用し、スタブはどちらの方法でも使用できます。Meszarosは、動作検証を使用するスタブをテストスパイと呼んでいます。違いは、ダブルがどのように実行および検証されるかであり、それは自分で調べてみることにします。
古典的テストとモックテスト
これで、2番目の二分法、つまり古典的なTDDとモック主義のTDDの違いを探ることができます。ここで大きな問題は、*いつ*モック(または他のダブル)を使用するかです。
**古典的なTDD**スタイルは、可能であれば実際のオブジェクトを使用し、実際のオブジェクトを使用するのが難しい場合はダブルを使用することです。そのため、古典的なTDDerは実際の倉庫を使用し、メールサービスにはダブルを使用します。ダブルの種類はそれほど重要ではありません。
しかし、**モック主義のTDD**を実践する人は、興味深い動作をするオブジェクトには常にモックを使用します。この場合は、倉庫とメールサービスの両方です。
さまざまなモックフレームワークはモック主義テストを念頭に置いて設計されましたが、多くの古典主義者はダブルを作成するのに役立つと考えています。
モック主義スタイルの重要な派生物は、ビヘイビア駆動開発(BDD)です。BDDは、もともと私の同僚であるDaniel Terhorst-Northによって、TDDが設計手法としてどのように機能するかに焦点を当てることで、人々がテスト駆動開発をよりよく学習できるようにするための手法として開発されました。これにより、テストの名前をビヘイビアに変更して、TDDがオブジェクトの動作について考えるのにどのように役立つかをよりよく探求できるようになりました。BDDはモック主義のアプローチを採用していますが、命名スタイルと分析をその手法に統合したいという願望の両方で、これを拡張しています。この記事との関連性は、BDDがモック主義テストを使用する傾向があるTDDの別のバリエーションであることだけなので、ここではこれ以上説明しません。詳細については、リンクをたどってください。
「古典的」には「デトロイト」スタイル、「モック主義」には「ロンドン」スタイルが使用されることがあります。これは、XPがもともとデトロイトのC3プロジェクトで開発され、モック主義スタイルがロンドンの初期のXP採用者によって開発されたという事実を暗示しています。また、多くのモック主義TDD担当者は、その用語、そして実際には古典的テストとモック主義テストの間に異なるスタイルがあることを意味する用語を嫌っています。彼らは、2つのスタイルの間に有用な区別があるとは考えていません。
違いによる選択
この記事では、状態検証または動作検証/古典的またはモック主義TDDの2つの違いについて説明しました。それらの間で選択を行う際に留意すべき引数は何ですか?まず、状態と動作の検証の選択から始めます。
最初に考慮すべきことはコンテキストです。注文と倉庫のような簡単なコラボレーション、または注文とメールサービスのような厄介なコラボレーションについて考えていますか?
簡単なコラボレーションであれば、選択は簡単です。私が古典的なTDDerであれば、モック、スタブ、またはその他のダブルは使用しません。実際のオブジェクトと状態検証を使用します。私がモック主義のTDDerであれば、モックと動作検証を使用します。まったく決定はありません。
厄介なコラボレーションであれば、私がモック主義者であれば決定はありません。モックと動作検証を使用するだけです。私が古典主義者であれば、選択肢はありますが、どちらを使用しても大した問題ではありません。通常、古典主義者は状況ごとに決定を下し、各状況で最も簡単なルートを使用します。
したがって、見てのとおり、状態と動作の検証は、ほとんどの場合、大きな決定ではありません。本当の問題は、古典的TDDとモック主義TDDの間です。結局のところ、状態と動作の検証の特性は、その議論に影響を与えます。それが私がエネルギーのほとんどを集中するところです。
しかし、その前に、エッジケースを投入させてください。厄介なコラボレーションではない場合でも、状態検証を実際に使用するのが難しいことに出くわすことがあります。これの素晴らしい例はキャッシュです。キャッシュのポイントは、状態からキャッシュがヒットしたかミスしたかを判断できないことです。これは、筋金入りの古典的なTDDerでさえ、動作検証が賢明な選択となるケースです。どちらの方向にも他にも例外があると思います。
古典的/モック主義の選択を掘り下げると、考慮すべき要素がたくさんあるので、それらを大まかなグループに分けました。
TDDの推進
モックオブジェクトはXPコミュニティから生まれました。XPの主な機能の1つは、テスト駆動開発を重視していることです。システム設計は、テストの作成によって駆動される反復を通じて進化します。
したがって、モック主義者が特にモック主義テストが設計に及ぼす影響について話していることは驚くべきことではありません。特に、彼らはニーズ駆動型開発と呼ばれるスタイルを提唱しています。このスタイルでは、システムの外部の最初のテストを作成し、インターフェースオブジェクトをSUTにすることからユーザーストーリーの開発を開始します。共同作業者への期待をじっくり考えることで、SUTと隣接するオブジェクト間の相互作用を探求し、SUTのアウトバウンドインターフェースを効果的に設計します。
最初のテストを実行すると、モックへの期待が次のステップの仕様とテストの開始点を提供します。各期待を共同作業者のテストに変換し、一度に1つのSUTずつシステムに取り組むプロセスを繰り返します。このスタイルは、外側から内側と呼ばれることもあり、非常にわかりやすい名前です。これは、階層型システムでうまく機能します。最初に、下にあるモックレイヤーを使用してUIのプログラミングを開始します。次に、下位レイヤーのテストを作成し、システムを一度に1レイヤーずつ徐々にステップ実行します。これは非常に構造化され、制御されたアプローチであり、多くの人がOOとTDDの初心者をガイドするのに役立つと信じています。
古典的なTDDは、まったく同じガイダンスを提供するわけではありません。モックの代わりにスタブメソッドを使用して、同様のステップアプローチを実行できます。これを行うには、共同作業者から何かが必要になるたびに、テストでSUTを動作させるために必要な応答をハードコーディングするだけです。次に、それでグリーンになったら、ハードコーディングされた応答を適切なコードに置き換えます。
しかし、古典的なTDDは他のこともできます。一般的なスタイルは、ミドルアウトです。このスタイルでは、機能を取得し、この機能を動作させるためにドメインで何が必要かを決定します。ドメインオブジェクトに必要なことを実行させ、それらが機能したら、UIを上に重ねます。これを行うと、何も偽造する必要がなくなる場合があります。多くの人がこれを気に入っています。これは、ドメインモデルに最初に注意を集中させるため、ドメインロジックがUIにリークするのを防ぐのに役立つためです。
モック主義者と古典主義者の両方がこれを一度に1つのストーリーで行うことを強調する必要があります。別のレイヤーが完了するまで1つのレイヤーを開始しない、レイヤーごとにアプリケーションを構築するという考え方があります。古典主義者とモック主義者の両方は、アジャイルなバックグラウンドを持ち、きめ細かい反復を好む傾向があります。その結果、彼らはレイヤーごとではなく、機能ごとに機能します。
フィクスチャの設定
古典的なTDDでは、SUTだけでなく、テストに応じてSUTが必要とするすべての共同作業者も作成する必要があります。この例では2つのオブジェクトしかありませんでしたが、実際のテストでは多くの場合、大量のセカンダリオブジェクトが関係します。通常、これらのオブジェクトは、テストが実行されるたびに作成および破棄されます。
ただし、モック主義テストでは、SUTとそのすぐ隣にあるモックを作成するだけで済みます。これにより、複雑なフィクスチャを構築する際に発生する作業のいくつかを回避できます(少なくとも理論的には。かなり複雑なモックセットアップの話に出くわしましたが、それはツールをうまく使用していないためかもしれません)。
実際には、古典的なテスターは複雑なフィクスチャをできるだけ再利用する傾向があります。最も簡単な方法は、フィクスチャのセットアップコードをxUnitのセットアップメソッドに配置することです。より複雑なフィクスチャは複数のテストクラスで使用される必要があるため、この場合は特別なフィクスチャ生成クラスを作成します。私は通常、Thoughtworks XPプロジェクトの初期に使用されていた命名規則に基づいて、これらをオブジェクトマザーと呼んでいます。マザーの使用は大規模な古典的テストでは不可欠ですが、マザーは保守が必要な追加コードであり、マザーへの変更はテストに大きな波及効果をもたらす可能性があります。フィクスチャのセットアップにはパフォーマンスコストがかかる場合がありますが、適切に行われれば深刻な問題になるとは聞いていません。ほとんどのフィクスチャオブジェクトは作成にコストがかかりませんが、そうでないものは通常はdouble化されます。
その結果、どちらのスタイルも相手を多大な労力だと非難しているのを耳にしました。モック主義者はフィクスチャの作成には多くの労力が必要だと言いますが、古典主義者はこれは再利用されるものの、すべてのテストでモックを作成する必要があると言います。
テストの分離
モック主義テストでシステムにバグを導入した場合、通常、バグを含むSUT(テスト対象システム)のテストのみが失敗します。しかし、古典的なアプローチでは、クライアントオブジェクトのテストも失敗する可能性があり、バグのあるオブジェクトが別のオブジェクトのテストでコラボレーターとして使用されている場合に障害が発生します。その結果、頻繁に使用されるオブジェクトの障害が、システム全体に波及してテストの失敗を引き起こします。
モック主義テスターはこれを大きな問題と考えています。エラーの根本原因を見つけて修正するには、多くのデバッグ作業が必要になります。しかし、古典主義者はこれを問題の根源として表明していません。通常、どのテストが失敗したかを確認することで、原因を比較的簡単に見つけることができ、開発者は他の失敗が根本的な障害に由来することを理解できます。さらに、定期的にテストを行っている場合(そうすべきですが)、最後の編集によって破損が発生したことがわかっているため、障害を見つけるのは難しくありません。
ここで重要な要素の1つは、テストの粒度です。古典的なテストでは複数の実際のオブジェクトを実行するため、多くの場合、1つのオブジェクトだけでなく、オブジェクトのクラスターの主要なテストとして単一のテストが見つかります。そのクラスターが多くのオブジェクトにまたがっている場合、バグの実際の原因を見つけるのがはるかに難しくなる可能性があります。ここで起こっているのは、テストの粒度が粗すぎることです。
モック主義テストはこの問題の影響を受けにくい可能性があります。なぜなら、プライマリ以外のすべてのオブジェクトをモックアウトするのが慣例であり、コラボレーターのためにより細かい粒度のテストが必要であることが明らかになるからです。とはいえ、粒度が粗すぎるテストを使用することは、必ずしも古典的なテスト手法の失敗ではなく、古典的なテストを適切に実行できなかったことであることも事実です。経験則として、すべてのクラスに細かい粒度のテストを分けておくことをお勧めします。クラスターが妥当な場合もありますが、ごく少数のオブジェクト(6個以内)に限定する必要があります。さらに、粒度が粗すぎるテストのためにデバッグの問題が発生した場合は、テスト駆動型でデバッグし、進行に合わせて粒度の細かいテストを作成する必要があります。
本質的に、古典的なxunitテストは単体テストだけでなく、ミニ統合テストでもあります。そのため、多くの人は、クライアントテストがオブジェクトのメインテストで見逃された可能性のあるエラー、特にクラスが相互作用する領域を調査するエラーをキャッチするという事実を気に入っています。モック主義テストはこの品質を失います。さらに、モック主義テストの期待が間違っている可能性があり、単体テストはグリーンで実行されますが、固有のエラーが隠されるリスクもあります。
この時点で、どのスタイルのテストを使用する場合でも、システム全体で動作するより粗い粒度の受け入れテストと組み合わせる必要があることを強調しておく必要があります。受け入れテストの使用が遅れて後悔したプロジェクトによく遭遇しました。
実装へのテストの結合
モック主義テストを作成する場合、SUTのアウトバウンド呼び出しをテストして、サプライヤーと適切に通信していることを確認します。古典的なテストは、最終状態のみを気にします。その状態がどのように導き出されたかは気にしません。したがって、モック主義テストはメソッドの実装により密接に結合されます。コラボレーターへの呼び出しの性質を変更すると、通常、モック主義テストは中断されます。
この結合は、いくつかの懸念事項につながります。最も重要なのは、テスト駆動開発への影響です。モック主義テストでは、テストを作成するときに動作の実装について考える必要があります。実際、モック主義テスターはこれを利点と考えています。しかし、古典主義者は、外部インターフェースから何が起こるかだけを考えて、テストの作成が完了するまで実装に関するすべての考慮事項を残しておくことが重要だと考えています。
実装への結合はリファクタリングも妨げます。実装の変更は、古典的なテストよりもテストを中断する可能性がはるかに高いためです。
これは、モックツールキットの性質によって悪化する可能性があります。多くの場合、モックツールは、特定のテストに関係がない場合でも、非常に具体的なメソッド呼び出しとパラメーターの一致を指定します。 jMockツールキットの目的の1つは、期待の仕様をより柔軟にして、問題のない領域で期待を緩めることです。ただし、リファクタリングをよりトリッキーにする可能性のある文字列を使用するという犠牲を払います。
設計スタイル
これらのテストスタイルの最も魅力的な側面の1つは、設計上の決定にどのように影響するかです。両方のタイプのテスターと話すにつれて、スタイルが促進する設計の違いに気づきましたが、表面を barely scratchingているだけだと思います。
レイヤーへの取り組み方の違いについてはすでに述べました。モック主義テストは外部からのアプローチをサポートしますが、ドメインモデルアウトスタイルを好む開発者は古典的なテストを好む傾向があります。
より小さなレベルでは、モック主義テスターは値を返すメソッドから離れて、収集オブジェクトに作用するメソッドを好む傾向があることに気づきました。オブジェクトのグループから情報を収集してレポート文字列を作成するという動作の例を見てみましょう。これを行う一般的な方法は、レポートメソッドにさまざまなオブジェクトの文字列戻りメソッドを呼び出させ、結果の文字列を一時変数にアセンブルすることです。モック主義テスターは、文字列バッファーをさまざまなオブジェクトに渡して、さまざまな文字列をバッファーに追加させる可能性が高くなります。文字列バッファーを収集パラメーターとして扱います。
モック主義テスターは、「列車の残骸」、つまり`getThis().getThat().getTheOther()`スタイルのメソッドチェーンを避けることについてより多く話します。メソッドチェーンを避けることは、デメテルの法則に従うこととしても知られています。メソッドチェーンは臭いですが、転送メソッドで肥大化した中間者オブジェクトの反対の問題も臭いです。(デメテルの法則がデメテルの提案と呼ばれれば、もっと気持ちが楽になるといつも感じていました。)
OO設計で人々が理解するのが最も難しいことの1つは、「尋ねるな、命じろ」原則です。これは、クライアントコードでデータを実行するためにオブジェクトからデータを抽出するのではなく、オブジェクトに何かを実行するように指示することをお勧めします。モック主義者は、モック主義テストを使用すると、これを促進し、最近のコードの多くに蔓延しているゲッターの混乱を回避できると述べています。古典主義者は、これを行うための他の多くの方法があると主張しています。
状態ベースの検証で認識されている問題は、検証をサポートするためだけにクエリメソッドが作成される可能性があることです。テストのためだけにオブジェクトのAPIにメソッドを追加することは決して快適ではありません。動作検証を使用すると、その問題を回避できます。これに対する反論は、そのような変更は実際には軽微であるということです。
モック主義者はロールインターフェースを好み、このスタイルのテストを使用するとロールインターフェースが増えることを主張しています。各コラボレーションは個別にモックされるため、ロールインターフェースになる可能性が高くなります。そのため、上記の文字列バッファーを使用してレポートを生成する例では、モック主義者はそのドメインで意味のある特定のロールを発明する可能性が高く、これは文字列バッファーによって実装される*可能性が*あります。
設計スタイルのこの違いが、ほとんどのモック主義者の主要な動機付け要因であることを覚えておくことが重要です。TDDの起源は、進化型設計をサポートする強力な自動回帰テストを取得したいという願望でした。その過程で、実践者はテストを最初に作成すると設計プロセスが大幅に改善されることを発見しました。モック主義者は、どのような設計が良い設計であるかについて強い考えを持っており、主に人々がこの設計スタイルを開発するのを助けるためにモックライブラリを開発しました。
古典主義者とモック主義者のどちらであるべきか?
この質問に自信を持って答えるのは難しいと感じています。個人的には、私はいつも昔ながらの古典的なTDDerであり、これまでのところ変更する理由が見つかりません。モック主義TDDの説得力のある利点が見当たらず、テストを実装に結合することの影響が懸念されます。
これは、モック主義プログラマーを観察したときに特に印象的でした。テストを作成している間、どのように行われるかではなく、動作の結果に焦点を当てるという事実が本当に気に入っています。モック主義者は、期待を書くためにSUTがどのように実装されるかについて常に考えています。これは私には本当に不自然に感じます。
また、おもちゃ以上のものに対してモック主義TDDを試していないという欠点にも悩まされています。テスト駆動開発自体から学んだように、真剣に試してみないとテクニックを判断するのは難しいことがよくあります。私は、非常に満足していて、モック主義者に確信を持っている多くの優れた開発者を知っています。ですから、私はまだ確信を持った古典主義者ですが、どちらの議論もできる限り公平に提示して、自分で判断できるようにしたいと思います。
そのため、モック主義テストが魅力的に聞こえる場合は、試してみることをお勧めします。モック主義TDDが改善することを目的としている領域のいくつかで問題が発生している場合は、特に試してみる価値があります。ここでは2つの主要な領域があります。1つは、テストがクリーンに中断されず、問題の場所がわからないために、テストの失敗時にデバッグに多くの時間を費やしている場合です。(細かい粒度のクラスターで古典的なTDDを使用することで、これを改善することもできます。)2つ目の領域は、オブジェクトに十分な動作が含まれていない場合、モック主義テストによって開発チームがより多くの動作豊富なオブジェクトを作成するようになる可能性があることです。
最後に
ユニットテスト、xunitフレームワーク、そしてテスト駆動開発への関心が高まるにつれ、モックオブジェクトに遭遇する人がますます増えています。多くの人は、モックオブジェクトフレームワークについて少し学びますが、その基礎にあるモッキスト/クラシカルの対立を完全に理解しているわけではありません。どちらの立場に立つかは別として、この見解の違いを理解することは有益だと思います。モックフレームワークを使いこなすためにモッキストである必要はありませんが、ソフトウェアの多くの設計決定を導く考え方を理解することは有用です。
この記事の目的は、これらの違いを指摘し、それらの間のトレードオフを明確にすることでした。そして、今もそうです。モッキストの考え方については、私が説明できた以上のものがあり、特にデザインスタイルへの影響があります。今後数年間で、このテーマに関するより多くの著作物が出版され、コードを書く前にテストを書くという魅力的な結果についての理解が深まることを願っています。
参考文献
xunitテストプラクティスの徹底的な概要については、Gerard Meszarosの forthcoming book(免責事項:私のシリーズです)にご期待ください。彼はまた、本書のパターンを掲載したウェブサイトを運営しています。
TDDについてもっと知るには、まずケントの本を参照してください。
モッキストスタイルのテストについてより詳しく知るには、最良のリソースはFreeman & Pryceです。著者はmockobjects.comを管理しています。特に素晴らしいOOPSLA論文を読んでください。モッキストスタイルのTDDの異なる派生である、振る舞い駆動開発についての詳細は、Daniel Terhorst-Northの紹介から始めてください。
jMock、nMock、EasyMock、そして.NET EasyMockのツールウェブサイトを見ることで、これらのテクニックについてより詳しく知ることができます。(他にもモックツールはありますので、このリストが完全であるとは思わないでください。)
XP2000ではオリジナルのモックオブジェクトの論文が発表されましたが、現在ではかなり時代遅れになっています。
主な改訂
2007年1月2日:状態ベースとインタラクションベースのテストの区別を、状態と振る舞いの検証、そしてクラシックとモッキストTDDの2つに分割しました。また、Gerard Meszarosのxunitパターンの本に合わせて、さまざまな語彙の変更を行いました。
2004年7月8日:初版