モック、スタブ、擬似オブジェクトを使った効率的なユニットテスト3

Pseudo Objects

擬似オブジェクト

Here's a scenario where mock objects may be a bit of overkill. Suppose you're going to test a method, and the path you're going to test shouldn't reference a particular object that is passed into the method as an argument. For example, say you want to test the boundary conditions in the following method:

今度はモックオブジェクトに手間がかかってしまうようなケースだ。あるメソッドをテストしようとしていて、テストしようとしている処理でメソッドの引数として渡されたオブジェクトを参照してはいけないとしよう。例えば、境界条件を下のメソッドでテストしたいとする。

public static Report createMonthlyReport(int month, int year, Account account) {
	if (month < 1 || month > 12) {
		throw new IllegalArgumentException();
	}
	if (year < 1975 || year > getCurrentYear()) {
		throw new IllegalArgumentException();
	}
	・・・
}

Your test needs to pass in values that will cause the IllegalArgumentException to be thrown. This path through the createMonthlyReport() method will not access the Account object that is passed in; in fact, you want to ensure that the method doesn't reference the Account object. You could pass in a mock object, but you would have to do a bit of setup in your test, as shown below:

例外としてスローされる IllegalArgumentException を起こすような値をテストする必要がある。createMonthlyReport()メソッドを通るこの処理は、渡されるAccountオブジェクトにアクセスしない。モックオブジェクトを渡すことはできるが、[モックオブジェクトと境界条件をあわせてテストするなら] 下のようにテストで少しセットアップを行なわないといけないだろう。

public void testMonthCannotBeNegative() {
	AccountMock account = new AccountMock();
	account.setExpectedGetBalanceCalls(0);
	account.setExpectedAddEntryCalls(0);
	account.setExpectedGetEntriesForCalls(0);
	// .. etc
	int month = -1;
	int year = 1999;
	try {
		ReportFactory.createMonthlyReport(month, year, account);
		fail("Exception should have been thrown on negative month");
	} catch (IllegalArgumentException e) {
		;
	}
}

This test ensures that none of the methods on the Account object is called, and it does this by setting the "expected calls" values for each method on the mock to 0.
Now, take a look at a simpler technique that accomplishes the same thing. Instead of creating a mock object that implements the Account interface, create what I call a "pseudo object." A pseudo object does nothing more than extend an interface and throw exceptions on any method call. In the case of Account, the AccountPseudo would look like this:

このテストはAccoutオブジェクトのどのメソッドも呼ばれないことを保証する。これはモックの各"expected calls" [setExpectedGetBalanceCalls,setExpectedAddEntryCalls,setExpectedGetEntriesForCalls] の値を0にセットすることで行われる。
今度は同じことを実現するもっと簡単なテクニックを見てみよう。Accountインターフェースを実装したモックオブジェクトを作るかわりに、私が”擬似オブジェクト”と呼ぶものを作るのだ。擬似オブジェクトはインターフェースを継承することと、どのメソッド呼び出しについても例外をスローする以外はなにもしない。Accountの場合、AccountPseudoはこのようになる。

public class AccountPseudo implements Account {
	public void addEntry(Date date, double amount) {
		throw new PseudoError();
	}
	public List getEntriesFor(int month, int year) {
		throw new PseudoError();
	}
	public double getBalance() {
		throw new PseudoError();
	}
}
public class PseudoError extends RuntimeException {

}

As you can see, this is definitely not a mock object. There are no methods defined above and beyond what is available on the interface. The pseudo doesn't remember any object state or verify anything during or after test execution; it simply throws an exception on any method call. Now, this is not necessarily a stub, because it is not providing any canned values that a test may use. But, as you'll see shortly, it is a very convenient class to extend when you need a stubs.
If you wanted to use a pseudo instead of the mock, your test code would look something like this:

見ての通り、これはモックオブジェクトではまったくない。インターフェースとして使えるメソッド以外のものは何も定義されていない。擬似オブジェクトはオブジェクトのどんな状態も記憶しないし、テストの実行中あるいは実行後に何も検証することもない。ただどのメソッドでも例外をスローするだけだからだ。そして、これはスタブというわけでもない。テストで使われそうな値を準備しておくこともしないからだ。しかし、すぐに分かるかもしれないが、これはスタブが必要な時にとても便利なのだ。

もし擬似オブジェクトをモックの代わりに使うとしたら、テストコードはこのようになるだろう。

public void testMonthCannotBeNegative () {
	Account account = new AccountPseudo();
	int month = -1;
	int year = 1999;
	try {
		ReportFactory.createMonthlyReport(month, year, account);
		fail("Exception should have been thrown on negative month");
	} catch (IllegalArgumentException e) {
		;
	}
}

That's a bit simpler, isn't it? By virtue of their construction, pseudos provide an inherent error-producing mechanism. By looking at the test, you can immediately tell that the test expects that the createMonthlyReport method will not operate on the Account object. How? Because if any code within the createMonthlyReport method calls any of the methods on the AccountPseudo, an exception will be thrown and the test will fail.
But that's not the only thing you can do with pseudo objects. As I mentioned earlier, you can also use a pseudo object to create a stub that returns canned data by extending the pseudo class and overriding its methods.

This unit test creates a stub by extending the AccountPseudo class and providing only the functionality needed for the test:

ずっとシンプルではないだろうか?擬似オブジェクトの構造のおかげで、おのずとエラー発生メカニズムを得ることができるのだ。このテストを見れば、createMonthlyReportメソッドがAccountオブジェクトに対する操作ができないことをテストが求めていることが直ちに分かるだろう。どうしてか?createMonthlyReportメソッドのどこかでAccountPseudoのメソッドのどれかを呼ぼうとしたら、例外がスローされてテストが失敗するからだ。
しかし擬似オブジェクトでできることはこれだけじゃない。前に述べたように、あらかじめ用意したデータを返すスタブを作るのにも擬似オブジェクトを使えるのだ。そのためには、擬似クラスを継承し、メソッドをオーバーライドすればいい。

public void testCreateMonthlyReport() {
	//setup
	Account account = new AccountPseudo() {
		public double getBalance() {
			return 55.5;
		}
	};
	int month = 2;
	int year = 1999;
	//execute
	Report actualReport = 
		ReportFactory.createMonthlyReport(month, year, account);
	//test
	Report expectedReport = createExpectedReport();
	assertEquals(expectedReport, actualReport);
}

The stub is created by overriding AccountPseudo using an anonymous inner class that exists directly in the test method. You could use a named inner class or a top-level class just the same, but using an anonymous inner class is more compact. In fact, the inline stub makes the test more self-contained and explicit.
Another advantage to using inline stubs is that you can quickly detect when your test, and in turn your code, is doing too much. If your anonymous inner class starts growing too big, that is a pretty good indication that you should consider doing a little refactoring in order to separate functional responsibility of the method that you're testing.

For example, a method may be responsible for creating objects and then acting on those objects. These responsibilities could be split into two fine-grain methods, one factory method and another that operates on the objects. Fine-grain methods are more easily tested and are more apt to be reused. This is generally considered better from a design perspective.

You can use both stubs and mocks in your code. I lean more towards state-based unit-testing and thus use stubs almost exclusively. There are many people who use mock object libraries to create their stubs. I find it easier to use stubs that extend from a pseudo class. This also deters me from using the mock methods to test the implementation details of my methods. If I am working on code that uses mocks and it is simpler to use stubs, I usually do "replace mock with stub" refactoring to get rid of the mock.

If you are doing a mixture of interaction and state-based unit testing, one thing to remember is that mocks can actually extend pseudos the same way that stubs do. This strategy works well if you are incrementally developing your own custom mock objects, and only mocking methods that are on the interface as they are needed by your tests. But, this is not very beneficial if you are auto-generating your mock objects with a code-generation tool.

このスタブはAccountPseudoを、テストメソッド内に直接存在する匿名内部クラスとしてAccountPseudoクラスをオーバーライドすることで作られる。名前を持つ内部クラスやトップレベルクラスでも同じようにできるが、匿名内部クラスを使う方がよりコードをコンパクトにできる。実際にインライン・スタブ [上の例のようにテストするメソッドの中でスタブとしてセットアップされるオブジェクト] はテストを自己完結した明快なものにしてくれる。
インライン・スタブを使う別の利点は、テストやコードでやっていることが多すぎるということをすぐに見つけられることだ。匿名内部クラスが大きくなりすぎたら、それはテストしているメソッドの機能の責務を分けるためのちょっとしたリファクタリングをすることを考える時であることのちょうどいい目安になる。

例えば、あるメソッドにはオブジェクトを生成する責任があり、そのオブジェクトにアクションをすることもあるだろう。こういった責任は粒度の細かい2つのメソッドに分割することができる。一つはファクトリーメソッドで、もう一つはオブジェクトへの操作を行う。粒度の細かいメソッドはテストをより簡単にできるし、再利用されるのにちょうどいい。一般的に設計の視点からのほうがこういったことは考えやすい。

コードの中にスタブとモックの両方を使うこともできる。私は状態中心ユニットテストをより好むので、もっぱらスタブを使う。スタブを作るのにモックオブジェクトライブラリを使う人も多いが、私は擬似クラスから拡張したスタブを使う方が簡単だと思う。またこのスタブは、メソッドの細かい実装までテストするモックメソッドを使うのを思いとどまらせてもくれる。もし私がモックを使ったコードを相手にしていて、スタブを使った方が簡単な時は、大抵モックを取り除くように”モックのスタブ置換”リファクタリングを行う。

もし相互作用中心のと状態中心のとが混ざったユニットテストを行うなら、スタブがやったのと同じようにモックも擬似オブジェクトを拡張すればいいということを、覚えておくといい。独自のカスタムモックオブジェクトを徐々に作りこんでいる途中で、テストの時と同じようにインターフェース上の擬似メソッドだけで必要な時、このやり方は効果を発揮する。しかし、コード生成ツールを使ってモックオブジェクトを自動生成するなら、この使い方はそれほど役には立たない。

Page 3 of 4