テストを書いているとモックオブジェクトを使う機会が多いと思います。そのモックオブジェクトは自前で作るよりは、JMockやMockito*1などのフレームワークを利用した方が楽でしょう。
今回は機能的に、ほぼ最強と思われるJMockitを紹介します。
これが、他のモックフレームワークとの機能比較です。
MockingToolkitComparisonMatrix - jmockit - A feature matrix comparing several mocking toolkits. - Project Hosting on Google Code
機能が多ければ使いやすいか。そんなことはないと思います。しかし、これは使いやすいかもと周りの人からお勧めがあったので、実際に使ってどんなところが使えるのか検証してみたので、書いてみます。あと、最後にScalaで使えるか試してみました。
あ、あと、id:ryoasaiさんのエントリもあわせて読むとよいかも。
次世代のモックフレームワークであるJMockitの基本的な使い方 - 達人プログラマーを目指して
準備
Maven2前提ですが、pom.xmlを以下のような感じに編集してみてください。
とりあえず、バージョンをプロパティ化します。
<properties> <jmockit.version>0.999.4</jmockit.version> </properties>
依存関係はこんな感じ。
<dependencies> <!-- 中略 --> <dependency> <groupId>mockit</groupId> <artifactId>jmockit</artifactId> <version>${jmockit.version}</version> <scope>test</scope> </dependency> <!-- 中略 --> </dependencies>
mvn test
でちゃんと実行させたいのであれば、以下のようにするよいでしょう。
<build> <!-- 中略 --> <plugin> <artifactId>maven-surefire-plugin</artifactId> <configuration> <argLine>-javaagent:"${settings.localRepository}"/mockit/jmockit/${jmockit.version}/jmockit-${jmockit.version}.jar</argLine> </configuration> </plugin> <!-- 中略 --> </build>
Eclipseでこのpom.xmlを読み込みます。m2eclipseならインポートすればOKです。
ということで、ポイントを絞って以下説明します。
不変オブジェクトはモックフレームワークと相性が悪い
不変なオブジェクトを作る場合は、finalなクラスにしないと思わぬところでハマる時があります。
人の名前を表すPersonNameクラスの不変オブジェクト、いわゆるバリューオブジェクトです。いろんなところで共有するので副作用が発生しないように、setterを持たず、フィールドもfinalにします。
public class PersonName { private final String firstName; private final String lastName; public PersonName(String firstName, String lastName) { this.firstName = firstName; this.lastName = lastName; } public String getFirstName() { return firstName; } public String getLastName() { return lastName; } // hashCode, equalsは省略 }
一見、不変オブジェクトなんですが、チーム内の誰が勝手にsetterないのは使いにくいじゃないか、といって、こんなクラスを作れてしまうわけです。そんなのあり得ない!と言っても、このクラスだと現実に可変なPersonNameができてしまいます。
public class MutablePersonName extends PersonName { private String firstName; private String lastName; public MutablePersonName() { super(null, null); } @Override public String getFirstName() { return firstName; } @Override public String getLastName() { return lastName; } // setterがあると同一インスタンスで状態が // 変えられてしまうので、不変ではなく可変になってしまう。 public void setFirstName(String firstName) { this.firstName = firstName; } public void setLastName(String lastName) { this.lastName = lastName; } // hashCode, equalsは省略 }
PersonName型にこのクラスのインスタンスが混じっていたら、まぁ不変という設計の前提は崩れるわけです。
void doProcessPersonName( Collection<PersonName> personNames ){ for( PersonName pn : personName ){ // pnは不変なの? 可変なやつが、混ざっている可能性あるかも。 } }
不用意に継承させてトラブらないようにするには、finalなクラスにするのがよいです。
public final class PersonName {
が、しかし、モックフレームワークでは、サブクラス化してモックを作るものが多いので、相性が悪いのです。この場合は、finalを付けずにやりすごずか、モック自体を使うのをやめるかですね。。
たとえば、Mockitoでは、finalなクラスはモック化をサポートしていません。
Cannot mock final classes
でも、JMockitなら心配ありません。
finalなDepartmentクラスを保存するリポジトリ DepartmentRepositoryのテストを書いてみました。
public class DepartmentRepositoryTest { @Mocked private Department department; // 複製したDepartment @Mocked private Department clonedDepartment; @Test public void test_エンティティを保存できること() { DepartmentRepository departmentRepository = new DepartmentRepository(); new NonStrictExpectations() { { department.getId(); result = UUID.randomUUID(); department.clone(); result = clonedDepartment; } }; departmentRepository.store(department); new Verifications() { { department.getId(); department.clone(); } }; } }
モックにしたいオブジェクトは、テストクラスのフィールドに宣言し、Mockedアノテーションを付けます。
振る舞いの定義は、new NonStrictExpectations() で、検証はnew Verifications() のコンストラクタブロックで記述します。メソッドが順番通りに呼び出されたか検証したい場合はVerificationsInOrderを使うとよいでしょう。この例では、department.getId() と department.clone()が呼ばれているかどうかをチェックしました。
リポジトリの実装はこんな感じ。
public class DepartmentRepository { private final Map<UUID, Department> departments = Maps.newHashMap(); public void store(Department department) { departments.put(department.getId(), department.clone()); } }
JUnitを実行するときは、Run ConfigurationでVMの引数を以下のように設定してください。*2
-javaagent:lib/jmockit-0.999.4.jar
実行するとグリーンバーです。
ということで、不変オブジェクトのモック化は簡単にできました。なんだったら、Stringクラスでもモック化できますよ。
スタティックなメソッドをモック化する
次は、スタティックなメソッドのモック化ですが、不変オブジェクトと同様にstaticなクラスメソッドだけを保持するユーティリティクラスに代表されるクラスは多くのモックフレームワークでモック化がサポートされていません。Mockitoも対応してません。たぶん、JMockも。
通常のモックフレームワークだと、ユーティリティクラスをラップするサービスを作ってモック化します。
public class FileServiceImpl implements FileService { public String readText(String path) { return FileUtil.readText(path); } } // FileUtilがモック化できないので、しかたなくFileServiceでラップする構造にして、それをモック化 FileService fileService = mock(FileService.class);
なんだかな。悩ましいですよね。そもそもユーティリティクラスを作らないとしても、使うライブラリのユーティリティクラスだったらどうしろとw
JMockitは、クラスメソッドでもモック化をサポートします。System.nanoTime()が返す値を必ず0Lにしてみました。System.currentTimeMillis()も同様にモック化できます。クラスメソッドが、自分で手を入れることができない場合に、特に効力を発揮しますね。
public class SystemNanoTimeTest { @Mocked final System system = null; @Test public void test() { new Expectations() { { System.nanoTime();result = 0L; } }; assertThat(System.nanoTime(), is(0L)); } }
プライベートなフィールドやメソッドもモック化できる。
以下のインメモリなリポジトリの内部にあるMapをモックにしたい場合はどうしたらよいでしょうか。やっぱり、パッケージプライベートでnewDepartmentMapメソッドを作ればよい?悩ましいですよね。
public class DepartmentRepositoryInMemory { private final Map<UUID, Department> departments = Maps.newHashMap(); // ここの部分をモック化したい // モックをやりたいがためにこんなコードを入れたくない、、、 //private final Map<UUID, Department> departments = newDepartmentMap(); //Map<UUID, Department> newDepartmentMap(){ // return Maps.newHashMap(); //} public void store(Department department) { departments.put(department.getId(), department.clone()); } }
JMockitなら、内部のプライベートなフィールドもモック化が可能です。プライベートなメソッドのモック化もできるようです。
当然、リフレクションでsetAccessible(true)なことやっていると思いますが、プロダクションのコードには入らないのでよいと思います。
詳しくはこちら。Accessing private fields, methods and constructors
public class DepartmentRepositoryTest { @Mocked private Department clonedDepartment; @Mocked private Department department; // DepartmentRepository内部のMapのモック @Mocked private Map<UUID, Department> departments; @Test public void test_エンティティを保存できること() { final UUID id = UUID.randomUUID(); final DepartmentRepository departmentRepository = new DepartmentRepository(); new NonStrictExpectations() { { department.getId(); result = id; department.clone(); result = clonedDepartment; // Mapをセットする setField(departmentRepository, departments); // Mapに対する振る舞いを記述 departments.put(id, clonedDepartment); } }; departmentRepository.store(department); new Verifications() { { department.getId(); department.clone(); // Mapの振る舞いを検証する departments.put(id, clonedDepartment); } }; // モックを使わずに内部のフィールドを直接参照して状態を検証することも可能。 // Map<UUID, Department> result = Deencapsulation.getField( // departmentRepository, "departments"); // assertThat(result.size(), is(1)); } }
パーシャルモックもできる
JMockにはなくて、Mockitoにあるパーシャルモック機能ですが、JMockitにもあります。
モックの振る舞い定義は結構面倒なんで、すでにあるオブジェクトの一部の振る舞いだけを再定義する感じです。以下は、インスタンス単位でやる方法ですが、クラスレベルでやる方法もあるようです。Partial Mocking
public class PartialMockTest { static class Person { private final Integer age; private final String name; Person(String name, Integer age) { this.name = name; this.age = age; } Integer getAge() { return age; } String getName() { return name; } } @Test public void test_partialMock() { final Person person = new Person("KATO", null); // すでにあるクラスのインスタンス new NonStrictExpectations(person) { // { person.getAge(); result = 123; } }; // Mocked: Person person = new Person("KATO", null); assertThat(person.getAge(), is(123)); // Not mocked: assertThat(person.getName(), is("KATO")); } }
Scalaでも使える
最後にScalaで使ってみました。
JUnitのテストクラスを以下のように書いて、Java同様にMockedアノテーションを付けると使えます。IntelliJでもテストが動きました。
class JMockitTest { @Mocked var hoge: Hoge = _ @Test def test() { new NonStrictExpectations() { { hoge.name val ret = "KATO" returns(ret, null) } } println(hoge.name) new Verifications() { { hoge.name } } } }
問題があったのは、振る舞い定義の部分。
NonStrictExpectationsで、resultがprocted staticなので、どうやってコード書けばよいかわからなかった。returnsメソッドがあって
returns(ret)
と記述できるはずですが、以下のようなコンパイルエラーがでてしまうので、
error: ambiguous reference to overloaded definition, both method returns in class Expectations of type (x$1: Any,x$2: <repeated...>[java.lang.Object])Unit and method returns in class Expectations of type (x$1: Any)Unit match argument types (java.lang.String) returns(ret)
以下のような書き方にしました。Javaのように書ければよいのですが。。
returns(ret, null)
使ってみた感想
個人的には、Mockitoのようなインターフェイスが好きですが、触っているうちにJMockitもいいかなって思うようになりました。まぁ、慣れれば別に気になりません。JMock派な人はコンストラクタブロックで書くのに慣れているし、読むのも苦労しないでしょう。
多機能なんで、他にも使えそうな機能があるのかもしれませんが、上記の機能だけでも使えるならすぐにでも使いたいと思いました。
今までのモックフレームワークでは、モックのためにプログラムコードの設計を歪めるのか、そもそもモックオブジェクトなしで頑張る、という極端な選択肢しかないわけです。理想的にはこの両方を回避したいということで、それができるのがJMockitだと思います。
追記:
>javaagent設定するのが結構面倒くさいんだよな
確かに面倒ですね。Eclipseでテストを実行する時に設定に加えないといけないですからね。デフォルト値を設定できるといいんですがねw
個人的には、この面倒臭さと上記の利点を天秤に掛けると利点のが優っているという感じです。プロダクトのVMの設定を弄るわけでもないし、現場では上記の問題で結構カオスな面もあるんで、これぐらいの面倒さは支払うよという感覚。
あわせて読みたい
次世代のモックフレームワークであるJMockitの基本的な使い方 - 達人プログラマーを目指して
Java - Jmockitでテストをしやすくする! - ip-community
Java - Jmockitでテストをしやすくする!2 - ip-community
Java - Jmockitでテストをしやすくする!3 - ip-community
Java - Jmockitでテストをしやすくする!4 - ip-community