mockery

前回のブログを書いたあとに、 @hidenorigoto 先生から指摘を受けてしまったので Mockeryを紹介してみるよ。
知らなかった自分が恥ずかしいが、PHP5.3以上に対応ということも助けて、かなり高機能だよ。

パフォーマンスとかはどうかはまだ体感していないですが。

static method のモック

本シリーズで紹介している通り、static method によるテスト阻害は
なかな苦しいものです。

しかし、ロード済みでないという制約下のもとで、Mockery による
static method のモックを作ることができます。
(やはり、クラスオートロード利用向けな予感がします。)
以下が例です。


SampleSingleton クラスが、仮にDBの接続系だったりすると、それを利用している
Sample::sfoo() などのテストはやりにくくなってしまいます。

なんとかなるよ! Mockery ならね。

なんという事でしょう。これだけで、OKです。

Prefixing the valid name of a class (which is NOT currently loaded) with “alias:” will generate an “alias mock”. Alias mocks create a class alias with the given classname to stdClass and are generally used to enable the mocking of public static methods. Expectations set on the new mock object which refer to static methods will be used by all static calls to this class.

(アホ超訳)

“alias:” というのを接頭語として正しいクラス名 (まだロードされていない) を利用すると、エイリアスモックを作成します。エイリアスモックは stdClass に対象クラス名でクラスエイリアスを作成します。それは一般的に public static メソッドのモッキングに有効です。static method を参照する新しいモックオブジェクトにセットされた expectations は、そのクラスのすべての static call が呼ばれたときに使われます。

なるほどなるほど。

ちなみに、PHP5.3新関数 の class_alias() という関数を駆使しているかんじです。
今回の例では、SampleSingleton をエイリアスとして、モックのクラスを読んでいます。
で、これまた PHP5.3 の新機能である __callStatic を使って何やらムフフやっている予感がします。

パーシャルモック

Mockery は、本当にリア充なので、 JMockit でできるようなパーシャルモックもやってのけます。

上の場合、Sample::bar() メソッドだけ、モックするよーという感じです。
Sample::foo() は維持してくれます。

残念なことに、private method のモック実現は runkit 使わないと難しいそうだけど。

まだまだ、ドキュメントを読み始めたばっかりなので、
面白い使い方があったらまた紹介します。

前回の Java編に引き続きまして
今回も、テスト対象コードを変えずに、頑張ってテストを書くにはどうしたらいいのかを考えてみます。
// もちろん、リファクタリングをするべきですが、リファクタリングの足掛かりとして
// テストを先に書いておきたいという思いがあるのです。

PHPでも Mock というか、Test Double を楽しむための方法が幾つかありそうなのでまとめてみる。

オートロードが利用できるケース

symfony1.x の、 sfContext (Singletonパターン) に関わるテストで利用されている手段です。クラスの定義してあるファイルを自動的にロードするための機能spl_autoload_register() で、登録した関数は、すでにクラスが定義済みであった場合は呼ばれません。

これを利用して、全く同名のモッククラスを定義したファイルを用意しておき、テスト時に require_once などで、そのファイルを呼び出してやるわけです。これで、対象とするクラスの動きとしてはモッククラスの動きになります。

非常でお気軽な方法ですが、モック対象が頻繁に変更するようなケースなどではオススメできないかもしれません。
すべてのメソッドを網羅している必要はなく、騙すだけで良いならばこの方法はいいかもしれません。


テキトーなので、autoload.php は PSR-0 などに全然沿っておりませんのでご了承ください。 test1.php に関しては、オートロードで lib/Sample.php がロードされることはなく、先に定義した Sample クラスが利用されていることがわかります。

runkit

JMockit のように、メソッドの一部のみの挙動を変更したい!というニーズに答えるならば runkit になるかと思います。 php モジュールになってしまいますが、メソッドの内容を書き換えたり、定数を消したりとなかなか魔術を感じる便利なものです。 ただし、PHP5.3 では pecl からインストールができないので svn からソースコードを落とし、手動でコンパイルしてやる必要があります。

$ svn co https://svn.php.net/repository/pecl/runkit/trunk/ runkit
$ cd runkit
$ phpize
$ ./configure
$ make 
$ sudo make install 

以下は、クラスの定義を書き換えている様子です。

runkit + PHPMockFunction

runkit を利用して、かつ JMock のように使いやすくやろうじゃないか的なライブラリもあるのですが、
どうやらメンテされてません。誰かクールなのを作りませんか? 言いだしっぺですかそうですか。

PHPMockFunction

PHPUnit の Mock

PHPUnit の Mock は、テスト可能なコードを書いていけば使い物になりますが、レガシーなコードのテストをまずは書いて進みたいという時には適さない様に思えます。

ちなみに、Symfony2 のコンポーネントは、この機能を利用しているため、テストもコードも非常に洗練されているような気がします。

【追記】次回予告

@hidenorigoto 先生にツッコミを受けたので、続編を書きます。mockery はどこまで出来るかわからないですけど、ドキュメント見た限り、なかなかクールでした!(ってか初めて知りました。)

この物語はフィクションです。そういうことにしておいてください。

こんにちは。Java訓練中の川原です。

テストがない。
そんな現実のレガシーコードと戦う事になったとします。
テストがないコードというのは、テストを実行することを考えて作られていないので、いざテストを書こうとすると(僕の技量がまだまだなことも大きいが)怖気づいてしまい、結局またテストが書けないという悪循環に陥る可能性があります。
改修するタイミングなどに、思い切って改修する場所の周辺だけでもテストを作っておきたいものです。

勇気必要ですよね。ケント・ベック本の「まえがき」にもあるとおりです。

こんなケースに出会った

今回は、 Java のケースを想定してください。


Controller.execute() の改修を行うので 、命綱として Controller のテストを書いておきたいとします。
名前の通り、 Singleton は、 Singleton Pattern を実装したクラスです。 例えば、データベースの接続状態などを管理し、データを取得するような機能を持つとかそういうのを想定してください。
Util はクラスメソッドをもっています。やはり、こちらも外部コンテンツに接続した結果が帰ってくるようなものを想定してください。
おまけに、 Controller には、 fetchData() という、これまた何が帰ってくるかわからないようなメソッドがあったと想定します。

Singleton.foo() 、 Util.bar() そして、 Controller.fetchData() は、テストとしてはなかなか扱いづらいです。しかも、ものすごくたくさんの場所で使われているとして、他のクラスがまったくテストを書いていないとすればいきなり全力でゴリゴリとリファクタリングしまくるのも辛いわけです。
(時間ないなら尚更辛い!)

そんな時に、Mock を書くことになるかと思いますが JMock では苦しい気がしました。なぜなら、JMock では Mock として利用されるインスタンスは、インターフェイス化されていないといけないからです。
また、 Util.bar() のようなクラスメソッドをモックする機能は JMock にはありません。
Java には、 Mockito という Mocking Framework もありますが、やはりクラスメソッドのモック化は厳しいような雰囲気でした。

というわけで、 現実と戦うためには JMockit が最強っぽい予感です。

たのしいモック

// Controller.java
package net.bucyou;

import org.apache.commons.lang3.RandomStringUtils;

public class Controller {
	public String execute() {
		return fetchData() 
			+ Singleton.getInstance().foo()
			+ Util.bar();
	}
	
	public String fetchData() {
		return RandomStringUtils.randomAscii(2);
	}
}
// Singleton.java
package net.bucyou;

import org.apache.commons.lang3.RandomStringUtils;

public class Singleton {
	private static Singleton singleton = null;
	
	private Singleton() {
		
	}
	
	public static Singleton getInstance() {
		if (null ==  Singleton.singleton) {
			Singleton.singleton = new Singleton();
		}
		
		return Singleton.singleton;
	}
	
	public String foo() {
		return RandomStringUtils.randomAscii(2);
	}
}
// Util.java
package net.bucyou;

import org.apache.commons.lang3.RandomStringUtils;

public class Util {
	public static String bar() {
		return RandomStringUtils.randomAscii(2);
	}
}

さて、厄介なコードを用意しました。
RandomStringUtils.randomAscii(2); に関しては、ランダムで2文字のAscii文字を返します。
当然、 Controller.execute() した結果として、何が帰ってくるかはわかりません。

// ControllerTest.java
package net.bucyou;

import static org.junit.Assert.*;
import static org.hamcrest.core.Is.is;
import org.junit.Test;

public class ControllerTest {

	@Test
	public void testExecute() {
		Controller controller = new Controller();
		
		assertThat(controller.execute(), is("AABBCC"));
	}

}

とりあえず書いたテストです。当然失敗です。

さて、モックを作る準備として、 jmockit.jar をビルドパスに組み込みます。

ドキュメントによると、 classpath の順序に注意らしいです。JUnit の前に持って行きましょう。

さらに、テスト実行時に -javaagent:jmockit.jar などと、 VM argument に追加してやる必要があるそうです。
(ココらへんが、どういう理由で必要なのかはまだちゃんと理解してないです。)

さぁ、モックを書こう!

// ControllerTest.java
package net.bucyou;

import static org.hamcrest.core.Is.is;
import static org.junit.Assert.assertThat;
import mockit.Expectations;
import mockit.Mocked;
import org.junit.Test;

public class ControllerTest {
	@Mocked
	final Util util = null;
	
	
	@Test
	public void testExecute() {
		final Controller controller = new Controller();
		
		// Expections は呼ばれる順番もチェックするぞ!
		new Expectations(controller) {
			Singleton singleton;
			{
				// controller.fetchData() のパーシャルモック
				// このメソッドだけ挙動変更とかができる。
				controller.fetchData();
				result = "AA";
				
				// Singleton のモック
				Singleton.getInstance();
				result = singleton;
				singleton.foo();
				result = "BB";
				
				// Util のモック
				Util.bar();
				result = "CC";
			}
		};
		
		assertThat(controller.execute(), is("AABBCC"));
	}
}

そして、実行。
ハハハ。美しい Test 成功のグリーンが輝いておる。

これでできた!

やったね!これで、テストを足掛かりに少しは前進することができるかなー。とか考えたわけです。
あと、モックを作るのは楽しいです!
JMockit は、カバレッジ計測を行ったりもできるらしいので、リア充な Mocking Framework だと思います。リア充乙。

ちなみに、これなんて読むんですか。
ジェイモキットとか読んでるんですけど。 JMock + Mockito で、ジェイモヒートとかなんですかね?

参考資料