クラウド名刺管理のSansan, Inc.




ソーシャル
Sansanのブログ
2012.01.22

モックとスタブの違い

カテゴリ
ヒト:エンジニア・技術職

こんばんは。開発部 kuma です。

先日社内勉強会でテスティングフレームワークの紹介があり、そこでモックとスタブの違いは何かについての話がありました。
人から聞くのは2回目だか3回目だかになるのですが、未だに理解し切れていない気がするので改めて調べてみました。



(いきなり)まとめ

Martin Fowler が Gerard Meszaros の定義を「広く普及すべき」と言っています。一つの答えとして考えて良いのでは無いでしょうか。

http://capsctrl.que.jp/kdmsnr/wiki/bliki/?TestDouble


・ダミーオブジェクトは、受け渡されることはあるが実際に使用されることはない。パラメータリストを埋めたいだけといった場合に利用されることが多い

・フェイクオブジェクトは、実際に動作するように実装されてはいるが、手抜きされているので製品版には向かない(InMemoryDatabaseが良い例である)。

・スタブは、テスト時の呼び出しに対して、あらかじめ用意された結果を返す。通常、テスト用にプログラムされたところ以外には応答しない。スタブは呼び出しの情報を記録することもある。例えば、Eメールゲートウェイスタブは「送られた」メッセージを記録するような場合だ。単に「送られた」メールの数を記録する場合もあるだろう。

・モックは、エクスペクテーションが事前にプログラムされたものである。エクスペクテーションとは、受信する一連の呼び出しの仕様を表わしたものである。期待されない呼び出しが行なわれた場合は例外をスローする。また、テスト実行後の検証(verification)で、期待された呼び出しがすべてきちんと行われたかどうかを確認する。

そして、これらをまとめて"Test Double"と読んでいます。



つまり

よくわかりません・・・。
スタブの意味するところは分かるのですがモックが難しいですね。

勉強会では検証機構の有無で分けるというような話だったと記憶していますが
なぜそれらを分けて考えているのでしょう?



こういうことでした

http://d.hatena.ne.jp/devbankh/20100210


モックとスタブの違いは本当は一番の問題ではない。最も興味深いのは、相互作用スタイル対状態スタイルというところだ。相互作用中心のテストを行うテスターは全てのサブオブジェクトについてモックを作る。状態中心のテスターは、実際のオブジェクトを使うのが現実的でないものについてのみスタブを作る。例えば、外部サービスやコストのかかるもの、状態中心のやり方では扱いにくいキャッシュのようなものだ。

スタブやモックというのはその言葉が意味するところは何か、と言えるほど明確な違いは無いようです。
オブジェクトの何をテストしようとしているのか(上記リンク先では「状態中心テスト」と「相互作用中心テスト」と呼ばれるテスト手法)によって
テスト時に作成する Test Dubule を分類出来るし、それを本質的な問題として考えるべきだと。



書いてみる

理解を深めるためには手を動かすのが一番です。
リンク先の内容そのままに、実際に C# & NUnit を利用してテストを書いてみます。

シナリオはリンク先に従い、


  • [注文]オブジェクトは[商品]オブジェクトとその注文数を保持する
  • [倉庫]オブジェクトは様々な[商品]オブジェクトとその在庫数を保持し、在庫の追加 / 引当を行う
  • 引当時、在庫があれば引当数分在庫が減少し、在庫が無ければ何もしない

とします。

実装は以下の通りです。


public class Product {
	public string Name { get; set; }		// 商品名
	public Product(string name) {
		Name = name;
	}
}

public class Order	{
	public int Count { get; set; }			// 数量
	public Product Product { get; set; }		// 商品
	public bool IsFilled { get; set; }

	public Order(string prodName, int count) {
		Product = new Product(prodName);
		Count = count;
	}
	public void Fill(Warehouse wareHouse) {
		IsFilled = wareHouse.HasInventry(Product.Name, Count);
		if (IsFilled)
			wareHouse.Remove(Product.Name, Count);
	}
}

public class Warehouse	{
	private Dictionary<string, int> _prodInventory = new Dictionary<string, int>();
	
	public virtual void Add(string prod, int quantity) {
		_prodInventory.Add(prod, quantity);
	}
	public virtual void Remove(string prodName, int quantity) {
		_prodInventory[prodName] -= quantity;
	}
	public virtual int GetInventry(string prodName) {
		return _prodInventory[prodName];
	}
	public virtual bool HasInventry(string prodName, int quantity) {
		return this.GetInventry(prodName) >= quantity;
	}
}

状態中心テスト

テスト対象オブジェクトの状態がどの様に遷移するかを検証するために行うテストを
「状態中心テスト」と呼び、テスト対象オブジェクトが都度都度取り得る状態の妥当性をつぶさに検証します。

上記のクラスに対して行う状態中心テストの例は以下の様になります。


public class 状態中心テスト {
	private readonly string PRODUCT_1 = "Product1";
	private Warehouse _warehouse = new Warehouse();

	[SetUp]
	public void 在庫を倉庫にセット() {
		_warehouse.Add(PRODUCT_1, 50);
	}
	public void 在庫が十分であれば引当でき_在庫が減少すべき() {
		// Arrange
		Order order = new Order(PRODUCT_1, 50);
		// Act
		order.Fill(_warehouse);
		// Assert
		Assert.IsTrue(order.IsFilled);
		Assert.AreEqual(0, _warehouse.GetInventry(PRODUCT_1));
	}
}

ここでは、在庫された商品を引当て、引き当てが行えたか?その結果在庫が無くなったか?を検証しています。
[注文]オブジェクトの状態(IsFilled)と[倉庫]オブジェクトの状態(Warehouse が保持する Key-Value オブジェクトにセットされた Value)
が想定通りに変化したかを確かめているわけですね。シンプルなテストです。



相互作用中心テスト

状態中心テストに対して相互作用中心テストは以下の様になります。
なお、モックの作成には Moq を使います。


public class 相互作用中心テスト {
	private static string PRODUCT_1 = "Product1";
	[Test]
	public void 在庫が十分であれば在庫引当処理が実行されるべき() {
		// Arrange
		var order = new Order(PRODUCT_1, 50);
		var warehouse = new Mock<Warehouse>();
		warehouse.Setup(m => m.HasInventry(It.IsAny<string>(), It.Is<int>(i => i == 50))).Returns(true).Verifiable();
		warehouse.Setup(m => m.Remove(It.IsAny<string>(), It.Is<int>(i => i == 50))).Verifiable();
		// Act
		order.Fill(warehouse.Object);
		// Assert
		warehouse.Verify();
		Assert.IsTrue(order.IsFilled);
	}
	[Test]
	public void 在庫が不十分であれば在庫引当処理が実行されないべき() {
		// Arrange
		var order = new Order(PRODUCT_1, 51);
		var warehouse = new Mock<Warehouse>();
		warehouse.Setup(m => m.HasInventry(It.IsAny<string>(), It.IsAny<int>())).Returns(false).Verifiable();
		warehouse.Verify(m => m.Remove(It.IsAny<string>(), It.IsAny<int>()), Times.Never());
		// Act
		order.Fill(warehouse.Object);
		// Assert
		warehouse.Verify();
		Assert.IsFalse(order.IsFilled);
	}
}

状態中心テストの例との違いがお分かりになるでしょうか?

Martin Fowler や上記リンクの訳者が言っているとおり、ここでの重要なポイントは引当を行わないケースにおいて
[倉庫]オブジェクトへのメソッド引数を It.IsAny (型さえあっていれば何でも良い)としていることです。

これはどういうことでしょうか?Martin Fowler の言を一部抜粋します。


1つ目のテストで[倉庫]オブジェクトに数字50が渡されることをチェックしているので、2つ目のテストでは同じ事を繰り返さなくてもいいからだ [渡された数字が50以外の数字かを判断しなくてもいい]

私だけかもしれませんが、正直さっぱりわかりませんね。意味するところを考えて見ます。

Moq が分かる方は気付いたかもしれませんが、実は相互作用中心テストでは
[倉庫]オブジェクトの在庫数がどうなったかは検証していません。

状態中心テストを見た後であるためわかりにくくなっていますが、
1つ目のテストケースで[注文]オブジェクト生成時に渡している50という注文数は実際の所なんでも構いません。
なぜなら、[倉庫]オブジェクトの HasInventry メソッドの返り値を true で固定しているからです。
仮に在庫数を超える注文数、60とか70で[注文オブジェクト]を生成してもテストは成功するでしょう。

では1つ目のテストケースで It.IsAny を使用していない理由は何でしょうか?
それは[注文]オブジェクトが持つ注文数と同じ値が[倉庫]オブジェクトに渡されていることを検証するためです。
(HasInventry と Remove の引数が、注文数と同じであることを確認している)

リンク先にも例がありませんでしたが、おそらくはこの後、[倉庫]オブジェクトが注文数と在庫数の関係を
正しく評価することを検証するテストを書くことになるでしょう。

以上から言える事は、相互作用中心テストでは特定条件下(今回で言えば注文数と在庫数の関係)において
関係する各オブジェクトがどの様に振る舞うかを検証するものである、ということです。



で、結局モックとスタブの違いは?

長くなってしまいましたので再度まとめます。


モックとは何か、スタブとは何か、ではなく、それぞれがどのように使われるか、ということだ。

身も蓋も無い結論ですが・・・。

雑な分類ですが、スタブは呼び出しに対して返す値やセットされる値そのもの、すなわちオブジェクト間の入出力を検証したい時に
作成するオブジェクトを指し、モックは特定条件下、極端な例としては正常系 / 異常系のようなテストケースにおいて期待される
振る舞い(ロジックですね)を検証できるオブジェクト、と考えれば十分ではないでしょうか。

それぞれが持つ背景や目指そうとしているテストの方向性が異なるため、それこそデザインパターンのように
コミュニケーションツールの一つとして、多少曖昧でも両者を分けて理解する事に意味はあると思います。

が、最近のテスティングフレームワークではモック / スタブのどちらかしか作成できないということはまずあり得ないでしょうし、
実際あまり意識過ぎるのもどうかな、という気がします。



おまけ

今回の趣旨からは外れるので特に話に挙げませんでしたが、リンク先ではテスト手法の説明の後に
それらが設計に及ぼす影響について論じています。大変勉強になる話なのでお時間のある方は是非読んでみて下さい。

また、今回例として実装した[倉庫]クラスのメソッドが全て virtual になっていることに気がついたでしょうか?
これは Moq を使うために仕方なく行った設計です。(他に全てのメソッドを Interface として切り出す方法もある)

ユニットテストを書く際によく問題になるテスト可能な設計と本来有るべき設計との乖離が顕著に現れている例ですが
どうしてもそれが嫌だという方は Moles というテスティングフレームワークを試してみて下さい。(お金がある方は Pex も)
対象が何であろうとフック可能なので、この設計でモック / スタブが作れるか?という心配をしないで済みます。
Moles については誰も書かないようであればそのうち書きたいです。

「ヒト:エンジニア・技術職」に関する記事
2012.01.31
Notepad++を使ってます
Notepad++を使ってます

はじめまして。開発部の永井です。 今日は今使っているテキストエディタ(ソースコードエディタ)の話です。これまでは meadow を使っていました。 以前はメインの開発にも meadow を使っていた...

2012.02.14
秀丸のアウトライン解析が地味に便利な件

みなさまはじめまして。開発部のyokotaです。Sansanのブログでは、初めてのポストですね。 ここでは何を書いたらいいのやら、いまだに整理ついてませんが、思ったままにつらつらと書いていこうかと思い...

2012.02.20
メールの誤送信を少しだけ事前に気づく Google Chrome 拡張機能
メールの誤送信を少しだけ事前に気づく Google Chrome 拡張機能

こんにちは、開発部でテスターをやってます @dany1468 です。 好きなレガシーコード改善パターンは仕様化テストです。 メールの誤送信って 怖いですよね、開発部だとそこまで外部の人とのやり取りっ...

2012.03.12
アーキテクトの仕事
アーキテクトの仕事

こんにちは。 Sansan開発部アーキテクチャ・インフラグループの藤倉です。 私の肩書きはアーキテクチャ・インフラグループのリーダーということになっていて、立ち位置としてはアーキテクト的なところ...

2012.01.04
年頭のご挨拶

あけましておめでとうございます。 Sansan開発部アーキテクチャ・インフラグループの藤倉です。 新年ということで今年一年の抱負を掲げました。 私の今年の抱負は『次元を超える』です。 昨年は『暴れ...

2011.12.29
ブログはじめました(3ヶ月ぶり、通算6回目)

  皆様初めまして。 記念すべき1エントリ目を書かせて頂くことになりました、Sansan 開発部所属の kuma です。 とうとう弊社開発部でもブログを始めることとなりました。 ...