JUnit実践入門のMockitoの部分をJMockit (version 1.37) でやってみた

Java アドベントカレンダー 2017 5日目の記事です。

以前書いた記事のJMockit のバージョンが古くていつか更新したいなぁと思っていたので更新しました!

bati11blog.hatenablog.com

この記事は、JUnit実践入門のMockitoについて説明してる部分をJMockitでやってみました、という内容です。

JMockitJUnitなどでテストを書くときに利用できるモックライブラリです。今回使ったJMockitのバージョンは1.37、JUnitのバージョンは4.12です。

準備

build.gradleに書きます。

testCompile 'org.jmockit:jmockit:1.37'
testCompile 'junit:junit:4.12'

書く順番に要注意なんてことが書いてあるので気をつけましょう・・・。

http://jmockit.org/gettingStarted.html#library

あとは、JUnitのテストクラスに @RunWith(JMockit.class) をつけます。

公式のチュートリアルはこちら。

http://jmockit.org/tutorial.html

スタブメソッドの定義

早速、以前のバージョンと違いがあります。 NonStrictExpectations がなくなったようです。代わりっぽいのがないので Expectations を使います。

    @Mocked
    List<String> stub;

    @Test
    public void スタブメソッドの定義() {
        new Expectations() {{
            stub.get(0); result = "Hello"; // スタブメソッドの定義
        }};
        assertEquals(stub.get(0), "Hello");
    }

例外を送出するスタブメソッド

以前は NonStrictExpectations を使ったサンプルコードを書いてました。 NonStrictExpectations の場合は呼び出し回数まで厳密に見ないのですが、 Expectations は定義したスタブ実装が呼び出されないとテストが失敗してしまいます。そのため、以下のテストは失敗します。

    @Test(expected = IndexOutOfBoundsException.class)
    public void 例外を送出するスタブメソッド() throws Exception {
        new Expectations() {{
            // NonStrictExpectationsが使えなくなった
            // Expectationsは定義した内容が呼ばれないとテストが失敗してしまう
            stub.get(0); result = "Hello";
            stub.get(1); result = "World";
            stub.get(2); result = new IndexOutOfBoundsException();
        }};
        stub.get(2);
    }

失敗した時のメッセージは以下のようになります。

Caused by: Missing 1 invocation to:
java.util.List#get(0)
   on mock instance: $Impl_List@5a39699c
instead got:
java.util.List#get(2)
    at SampleTest$2.<init>(SampleTest.java:38)
    at SampleTest.例外を送出するスタブメソッド(SampleTest.java:33)
Caused by: Missing invocations
    at SampleTest$2.<init>(SampleTest.java:36)
    at SampleTest.例外を送出するスタブメソッド(SampleTest.java:33)
Caused by: java.lang.IndexOutOfBoundsException
    at SampleTest.例外を送出するスタブメソッド(SampleTest.java:40)

以下のように、呼び出さないスタブ実装を消してあげればテストが通ります。

    @Test(expected = IndexOutOfBoundsException.class)
    public void 例外を送出するスタブメソッド() throws Exception {
        new Expectations() {{
            // NonStrictExpectationsが使えなくなった
            // Expectationsは定義した内容が呼ばれないとテストが失敗してしまう
//            stub.get(0); result = "Hello";
//            stub.get(1); result = "World";
            stub.get(2); result = new IndexOutOfBoundsException();
        }};
        stub.get(2);
    }

void型を返すスタブメソッド

    @Test(expected=RuntimeException.class)
    public void メソッドの戻り値がvoidのスタブメソッド() {
        new Expectations() {{
            stub.clear(); result = new RuntimeException();
        }};
        stub.clear();
    }

任意の引数に対するスタブメソッド

メソッドの引数にスタブオブジェクトを指定する方式に書き換えます。以前は引数に指定する場合は @Mocked アノテーションがなくても大丈夫だったのですが、

    @Test
    public void 任意の整数に対するスタブメソッド(/* Mockedアノテーションが必要 */ @Mocked final List<String> stub) {
        new Expectations() {{
            stub.get(anyInt); result = "Hello";
        }};
        assertEquals(stub.get(0), "Hello");
        assertEquals(stub.get(1), "Hello");
        assertEquals(stub.get(999), "Hello");
    }

ちなみに @Mocked をつけなかった時のメッセージは以下なので、気がつかないとハマりそうです。。

java.lang.Exception: Method 任意の整数に対するスタブメソッド should have no parameters

スタブメソッドの検証

特に変わらず Verifications は使えました。

    @Test
    public void スタブメソッドの検証(@Mocked final List<String> mock) {
        mock.clear();
        mock.add("Hello");
        mock.add("Hello");
        new Verifications() {{
            mock.clear(); times = 1;
            mock.add("Hello"); times = 2;
            mock.add("World"); times = 0;
        }};
    }

Exceptations を使う方法も変わらず。

    @Test
    public void スタブメソッドの検証_Exceptationsで指定(@Mocked final List<String> mock) {
        new Expectations() {{
            mock.clear();      times=1;
            mock.add("Hello"); times=2;
            mock.add("World"); times=0;
        }};
        mock.clear();
        mock.add("Hello");
        mock.add("Hello");
    }

部分的なモックオブジェクト

これがうまくいきませんでした! ArrayListsize() だけ書き換えたい場合です。

以下のページを読むとこれで良さそうなのですが、 StackOverflowError が起きてしまいました。原因が分からず・・・。

http://jmockit.org/tutorial/Faking.html

    @Test
    public void 部分的なモックオブジェクト() {
        new MockUp<ArrayList<String>>() {
            @Mock
            public int size() {
                return 100;
            }
        };
        final List<String> mock = new ArrayList<>();

        mock.add("Hello");
        assertEquals(mock.get(0), "Hello");
        assertEquals(mock.size(), 100);
    }

2017/12/13 追記

ちなみに、ArrayListではなく、例えばStackを使った場合はうまくいきました。。

    @Test
    public void 部分的なモックオブジェクト() {
        new MockUp<Stack<String>>() {
            @Mock
            public int size() {
                return 100;
            }
        };
        final Stack<String> mock = new Stack<>();

        mock.push("Hello");
        assertEquals(mock.firstElement, "Hello");
        assertEquals(mock.size(), 100);

スパイオブジェクト

こちらは以前から特に変わらず。

    private static class SpyExample {
        Logger logger = Logger.getLogger(this.getClass().getName());
        public void doSomething() {
            logger.info("doSomething");
        }
    }

    @Test
    public void JMockitのMockUpAPIを使ったテスト() {
        // Setup
        SpyExample sut = new SpyExample();
        final StringBuilder infoLog = new StringBuilder();
        new MockUp<Logger>() {
            @Mock
            public void info(Invocation invocation, String message) {
                infoLog.append(message);
                invocation.proceed();
            }
        };
        sut.logger = Logger.getLogger(this.getClass().getName());

        // Exercise
        sut.doSomething();

        // Verify
        assertEquals(infoLog.toString(), "doSomething");
    }

おしまい

うーむ、部分的に書き換える方法がうまくいかないのは悔しいですね。また時間を見つけて調べてみたいと思います!