読者です 読者をやめる 読者になる 読者になる

愛と勇気と缶ビール

ふしぎとぼくらはなにをしたらよいか

Mockitoの新機能を使ったモダンげな使い方

Javaのmock用ライブラリは探せば色々あるが、一番よく使われてそう(自分調べ)ってことでMockitoを使っている。

http://mockito.org/

Mockitoはそのロゴからいっても「モキート」「モッキート」と読むのが正しいのだろうけど、心の中ではいつでも「モック伊藤」「モック伊藤」と呼んでしまう。

それはさておき、Mockitoについて日本語で書かれた記事は「mock()でモックしてverify()でベリファイするんやで〜」くらいで留まっているものが多そうに見受けられたので、新し目の機能を使ってこういう風に書くのもよさげかな、とここに記しておく。

参考: Mockito (Mockito 2.0.33-beta API)

mock()ではなく@Mock, spy()ではなく@Spyをつかう

以下のようにmock()メソッドを使ってmockを作ることも出来るのだが、

MyService service = mock(MyService.class);

Mockitoにはアノテーションでmockを作成する機能があるため、テストクラスの最初にフィールドとして以下のようにして書いておく。

public class MyServiceTest {
    @Mock
    MyService service;
}

こうすると、あるオマジナイを書いておくことでMockitoが各テストケースの前にmockを作成してくれるようになる。(オマジナイについては後述) spyについても同様のことが可能。

answerとかもちゃんと指定できる。

public class MyServiceTest {
    @Mock(answer = Answers.RETURNS_DEEP_STUBS;
    MyService service;
}

mock()やspy()を使っても同様のことが出来るのになぜこちらを選ぶのか。それはテストのコードを初めて見た人が「このクラスのテストではこいつらをmockする」とすぐに理解しやすいと考えるから。もちろん各テストケースの冒頭にmock(),mock()と繰り返し書いてあってもちゃんと読めば分かるが、クラスの先頭にまとめて書いてあった方が「テストケース共通でこいつらはmockされている」というのが分かりやすい。

@InjectMocksを使ってDIしてもらう (Whiteboxは使わない)

テスト対象のクラスが別のクラスのインスタンスに依存している、つまりprivateフィールドに別のクラスのインスタンスをコンポジットする感じになっている、というのはあるあるケースだと思う。

public class MyService {
    @Inject
    private OtherService otherService;
}

依存先クラスの単体テストは別に書くので、こいつもmockして置き換えたいなーと思うのが人情なのだが、これをMockito付属のWhiteboxというユーティリティクラスを使って書くとこんな感じになってしまう。

OtherService otherService = mock(OtherService.class);
Whitebox.setInternalState(service, "otherService", otherService);

フィールド名の文字列による指定がいただけない。これは@InjectMocksで置き換えられる。

public class MyServiceTest {
    @Spy
    @InjectMocks
    MyService service;

    @Mock
    OtherService otherService;
}

こう書くと、MyServiceのフィールドにOtherServiceクラスのmockされたインスタンスがDIされる。 これも先程と同様に、手続き的に行うよりも意図が分かりやすい。

どういった種類のインジェクションが可能なのかはInjectMocks (Mockito 2.0.33-beta API)を参照。

MockitoRuleを使う

以上の@Mock, @Spy, @InjectMocksを動作させるためにはオマジナイというか初期化処理が必要なのだが、それをJUnit4のRuleで行うためのMockitoRuleが用意されているのでそれを使う。

public class MyServiceTest {
    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    @Spy
    @InjectMocks
    MyService service;

    @Mock
    OtherService otherService;
}

Matchersを使う

今まで書いたのとは若干毛色が違うが、「テキトーな値」を指定するためのMatcherが色々Mockitoには用意されているのでデータをわざわざ用意したくない場合はそれを使う。

doReturn(1).when(this.service).doSomething(anyString()); // どんなStringが渡ってきても1を返す

any(Class clazz)やisNull()などなど、おおむね想像通りのMatcherが用意されている。

Matchers (Mockito 2.0.33-beta API)

注意点として、Matcherを使った引数と通常の引数を混ぜて指定することは出来ない。 つまり以下のように書いてしまうとMockitoに怒られてエラーになる(ちなみにMockitoのエラーメッセージは結構親切だと思う)

doReturn(1).when(this.service).doSomething2(anyString(), 1);

それだと不便じゃね?と思うかもしれないが、そういう場合はeq()を使って引数をMatcherに統一すればよい。

doReturn(1).when(this.service).doSomething2(anyString(), eq(1));

AdditionalMatchersをインポートすれば、配列に対するMatcherなども使えるようになる。

AdditionalMatchers (Mockito API)

今までのまとめ

以上のことをまとめた上で更にちょっと付け足すと、テストクラスは大体以下の様な書き方になる。

public class MyServiceTest {
    // 初期化のためのおまじない
    @Rule
    public MockitoRule mockito = MockitoJUnit.rule();

    // テスト対象のクラス
    @Spy
    @InjectMocks
    MyService service;

    // テスト対象のクラスが依存している(フィールドに持っている)クラス
    @Mock
    OtherService otherService;

    // テストケース間で共通のデータ
    long userId = 1L;

    @Before
    public void setUp() {
        // テストケース間で共通のモック処理はここでやっておく
        when(this.otherService.foo(anyString()).thenReturn("");
        when(this.otherService.bar(any(MyBean.class)).thenReturn(1);
    }

    @Test
    public void testDoSomething() throws Exception {
        // 各テストケースで固有のモック処理はここでやる
        doReturn(1).when(this.service).doSomething(anyString(), eq(1));

        // assertion
    }
}

好みの問題もあるだろうが、各テストケース内に色んなものをダラダラっと書くよりは「このクラスのテストではこうします」という意図は伝わりやすくなるかと思う。