mockito
http://code.google.com/p/mockito/
モックをお手軽に作れるライブラリ。
結構前からありますので情報もいっぱい出てますし、Exampleを見て書いてみると十分実用的なので使いたくなると思います。
早速試しましょう。
こういう例でおなじみの図形を例にします。
図形は○とか△とか□とかありますが、共通する部分は、面積を持っていることです。
計算方法は異なりますが、面積を求めることができます。
なので、面積を取得する操作を持たせます。
IShape.java
package myapp.mockito.example.shape; public interface IShape { double getArea(); }
(色々事情があって)先に図形を使ったテストをするとしましょう。
でもまだ○とか△とか□とか作ってないよ!!
おとといぐらいの自分は、こんなテストを描きました。
package myapp.mockito.example.shape; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import myapp.mockito.example.shape.IShape; import org.junit.Test; public class ShapeTest { @Test public void 面積取得() { IShape shape = new IShape() { @Override public double getArea() { return 12345.67890; } }; assertThat(shape.getArea(), is(12345.67890)); } }
無名クラスを使いました。
確かにIShapeのgetAreaで取得できる期待値を"12345.67890"という設定でテストを行えます。
しかし、この方法は実装するメソッドが1個であればよいですが、
複数のメソッドを実装する必要があるインタフェースもザラにあります。(Connectionインタフェースとか)
不要なインタフェースも実装しないといけないのは面倒ですね。
どうすればいいでしょうか。
すべて実装しないですむ方法に、Proxyを使う方法もあります。
package myapp.mockito.example.shape; import static org.hamcrest.CoreMatchers.is; import static org.junit.Assert.assertThat; import static org.junit.Assert.fail; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import myapp.mockito.example.shape.IShape; import org.junit.Test; public class ShapeTest { @Test public void 面積取得() { IShape shape = (IShape) Proxy.newProxyInstance( Thread.currentThread().getContextClassLoader(), new Class<?>[] { IShape.class }, new InvocationHandler() { @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { if (method.getName().equals("getArea")) { return 12345.67890; } fail(); return null; } }); assertThat(shape.getArea(), is(12345.67890)); } }
これはひどい。
複数のメソッドを実装する場合や、引数つきのテストの場合などを考慮すると、可読性がさらに落ちます。
考えたくないですね。
他にも継承を使って、Mockオブジェクトを作るやり方などもありますが、
よほど継承先クラスがMockとしての機能が充実していたり、枯れていない限り、無意味でしょう。
誰かの気分で変更を加えたら動かなくなったりね・・・
そこでmockitです。
mockitならこんな風にかけます。
package myapp.mockito.example.shape.impl; import static org.hamcrest.CoreMatchers.is; import static org.hamcrest.MatcherAssert.assertThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; import myapp.mockito.example.shape.IShape; import org.junit.Test; public class ShapeTest { @Test public void 面積取得() { IShape shape = mock(IShape.class); when(shape.getArea()).thenReturn(12345.67890); assertThat(shape.getArea(), is(12345.67890)); } }
やっている意味は非常に伝わってきますね。
こうやって決めた値を返すmockを作り、
mockを使ったプログラムが期待通りの動作をするか、
といったテストが簡単に書けるようになります。
他にも様々なことが出来るようですが、出てから大分経っているので先人たちに任せます。
簡単に書けるだけでなく、mockitoの使い方の流れを知っていれば、
他のプロジェクトに行ってもmockitoでかかれているコードであれば理解できるので
共通のMockフレームワークとして採用するのもアリなのかなーとも思います。
Javaしか認めない頑固な客先でも、mockitoはJavaで書かれているので、groovyに難色を示す人でも、好意的に受け入れられると思います。
(Power assertだけでも使う価値あるんじゃないかって思ってるんだけど・・・)
shape.getArea()とメソッドの呼び出しをしたあとなのに、なぜ値が設定できてるんだろう?
と、内部の動作が非常に木になっているので、調べる。
簡単にまとめると、Proxyを作ってMethodの処理を見張って呼び出したタイミングでその呼び出されたメソッドの情報を記憶して、
何らかの方法で(when, thenReturn時に)呼び出されたメソッドの情報をひっぱりだしてきて、その情報に紐づけて値を設定している。
shapeはmock()により作られたMockの管理下にあるProxyなインスタンスなので、Methodの呼び出しを監視して、その情報を裏で共有できる。
この場合ならgetAreaとやれば、裏でメソッド名がshapeインスタンスのgetArea(引数なし)といった情報が呼び出されたことをMockで管理される。
んで、when(shape.getArea())を行うと、内部でOngoingStubbing
ちなみに、shape.getArea()はこの時点では設定がされていないので初期値である値(doubleなら0.0)を返す。
ただ、この戻り値の型がわかることで、型推論が働き、その先のthenReturnの設定に必要な型情報を指定することができている。
thenReturnで、何かうまいことをやってshapeのgetArea()の情報を取り出して、これを戻り値に設定する。
(when内の返す情報に含めているか、thenReturnでshape.getArea()を呼び出した時に設定された情報を拾ってるかだと思うけど...)
以降、shape.getArea()はthenReturnで設定した値を返せるようになる。
結局何かうまいことやってる部分が重要なのに、さっぱりわかってない。
スレッドローカルなオブジェクトを作ってるので、そこでいい感じに管理してると思うんだけど。
読み解く力が足りないなー。