モックとスタブの違い 相互作用中心のテスト

相互作用中心のテスト

今度は同じ振る舞いと使い方を相互作用中心のテストでやってみる。このコードでは、モックを定義するのにjMockライブラリを使っている。jMockJavaで作られたものでは比較的新しいライブラリである。

public class OrderInteractionTester extends MockObjectTestCase {
	private static String TALISKER = "Talisker";

	public void testFillingRemovesInventoryIfInStock() {
		//setup
		Order order = new Order(TALISKER, 50);
		Mock warehouse = new Mock(Warehouse.class);
		
		//expectations
		warehouse.expects(once()).method("hasInventory")
			.with(eq(TALISKER),eq(50))
			.will(returnValue(true));
		warehouse.expects(once()).method("remove")
			.with(eq(TALISKER), eq(50))
			.after("hasInventory");

		//execute
		order.fill((Warehouse) warehouse.proxy());
		
		//verify
		warehouse.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());
	}
}

相互作用中心のテストには状態中心のテストと違うパターンがある。まず 1つ目のtestFillingRemovesInventoryIfInStock メソッドについてだけ考えよう。2つ目のテスト [testFillingDoesNotRemoveIfNotEnoughInStock()] では2つのショートカット [近道] をしているからだ [1つ目ではショートカットを使わない方法をまず説明する]。

フィクスチャののセットアップから始めるところがかなり違っている [上の状態中心のテストではオブジェクトの生成はsetUp()メソッドでまとめて行っていて、各メソッドでは生成処理を行わなかった]。メインオブジェクトについてだけ通常の [=テスト用でない] インスタンスを生成し、サブオブジェクトの通常のインスタンスは生成しない。そのかわり、モックインスタンスを生成する。

モックができたら、モックに想定 [expectation] を付け加える。この想定は、メインオブジェクトが動かされた時に、モックのどのメソッドが呼ばれるかを指定するものだ。想定はスタブとモックの一番の違いだ。

全ての想定が準備されたら、メインオブジェクトを動かすことができる。起動の後では、2つのことを行う。[1つ目は] 状態中心テストと同じようにメインオブジェクトに対してアサートをかける。一方で [2つ目に]、モックについても想定にしたがって呼び出しがされたかをチェックする。

これで状態中心テストと相互作用中心テストという名前が何に由来するかわかってもらえたと思う。状態中心テストではテストの結果を、メッセージング [原文ではstimulus{刺激};外部{から,へ}の刺激ということでメッセージングと訳す] の後で状態を調べることでチェックする。相互作用中心テストでは正しい相互作用がメッセージングによって起きたかをチェックする。もしメインオブジェクトの状態がメッセージングによって変わらなければ、アサートでは何もできず、モックを検証することで全てのことがチェックされる。

2つ目のテスト [testFillingDoesNotRemoveIfNotEnoughInStock()] では一つ目とは2つ違うことをしている。まずモックの生成の仕方が違う。コンストラクタではなくMockObjectTestCaseのmockメソッドを使って生成しているのだ。これはjMockライブラリに含まれる便利なメソッドで、これを使って生成されたモックオブジェクトはテストの最後で自動的に検証される [verified] ので、verify() を明示的に呼ぶ必要がないのだ。1つ目のテストでもこうすることができたが、モックを使ってどうテストするのかを説明するため、検証をより明示したかったので、そうしなかった。

訳者注:この次が相互作用中心テストの理解に重要だと思う

  • なぜ2つ目のテストで呼ばれるhasInventoryの引数がなんでもいいのかを理解する

2つ目のテストケースで違う点の2つ目は、withAnyArguments() を使うことで想定に対する制約を気にしなくていいことだ。こうする理由は、1つ目のテストでwarehouseオブジェクトに数字50が渡されることをチェックしているので、2つ目のテストでは同じ事を繰り返さなくてもいいからだ [渡された数字が50以外の数字かを判断しなくてもいい]。もし後でorderオブジェクトのロジックを変えないといけない [在庫があるケースのロジックが変わる or 在庫がないケースのロジックが変わる] 場合は、1つのテストだけが失敗するので、テスト変更の労力を少なくしてくれる。

EasyMockを使う

世の中には数多くのモックオブジェクトライブラリが存在する。よく聞くのはJava版も.NET版もあるEasyMockだ。EasyMockは相互作用中心のテストを促進するが、2つの点で論ずるに値する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
		Order order = new Order(TALISKER, 50);
		
		//expectations
		warehouseMock.hasInventory(TALISKER, 50);
		warehouseControl.setReturnValue(true);
		warehouseMock.remove(TALISKER, 50);
		warehouseControl.replay();

		//execute
		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は想定を設定するのに、record/replay [記録/再現] メタファを使っていて、まずモックで隠蔽したいオブジェクトのそれぞれに対してコントロールオブジェクトとモックオブジェクトを作る。モックオブジェクトの方だけでサブオブジェクトのインターフェースを満たし、コントロールオブジェクトは追加機能を提供する。想定を指定するには、予期される引数を引数にして、モックのメソッドを呼ぶ。戻り値がほしいなら、この後にコントロールへの呼び出しを行う。想定の設定を終えたら、コントロールのreplay()を呼ぶ。この時点でモックは記録 [recording] を完了してメインオブジェクト [からの呼び出し] に応答する準備が整う。そして [メインオブジェクトからの呼び出しがあった後] コントロールのverify()を呼び出す。

このrecord/replayメタファを初めて見て気後れする人たちがよくいるようだが、それにもすぐに慣れる。jMockがメソッド名の文字列でなくメソッドを指定しないといけないが、EasyMockはモックオブジェクトの実際のメソッドとして呼び出せるようにできる点で有利だ。これによってIDEのコード補完機能が使えるし、メソッドをリファクタリングすれば自動的に [IDEの機能で] テスト [コード中のメソッド呼び出し] が更新される。逆にEasyMockの弱い点は、jMockの [withAnyArgumentsのような] ルーズな制約をかけられないことだ。