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

文系プログラマによるTIPSブログ

文系プログラマ脳の私が開発現場で学んだ事やプログラミングのTIPSをまとめています。

今更SaStrutsのユニットテストとモックの残念な関係に泣く

seasar java ユニットテスト

数年前に一世を風靡したSaStruts、今でも現役で沢山使われていると思います。

このSaStrutsでもユニットテストをしたいですね。
中でもモックは非常に重要になります。

例えばDBのデータがこういう時にこういう結果になる、というケースです。
DBのデータは毎日変わってしまうので、普遍となる結果が欲しい、そういう時にモックを使います。

javaのモック

主に以下があります。

  • EasyMock
  • mockito
  • PowerMock
  • JMockit

上のものほど制限が多く、下のものほど制限が無くなります。
ここは迷わずJMockitを選択したいところです。

しかしどうも挙動が怪しいのです。

JMockitの場合

挙動がおかしいソース

package test.action;
import mockit.NonStrictExpectations;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seasar.framework.unit.Seasar2;
import org.seasar.framework.unit.TestContext;

@RunWith(Seasar2.class)
public class JMockit3Test {
    private TestContext ctx;
    private IndexAction action;
    public void before() {
        ctx.register(IndexAction.class);
    }

    @Test
    public void テスト() {
        new NonStrictExpectations() {{
            action.testLogic.getFirstName(); result = "jmockit";
        }};
        action.index();
        System.out.println(action.firstName);
    }
}

しかしこれを実行すると以下になります。

java.lang.IllegalStateException: Invalid place to record expectations

勿論ドキュメントにあるように、jmockitのjarをjunitより上にくるよう調整済みです。
どうも

@RunWith(Seasar2.class)

が悪さをしているようで、これを外すと動きます。
しかしSeasar2のテストランナーが外れると、各種DIが全く行われなくなるので、本来DIされるところを全て自力でモック化しないといけなくなります。

色々試してみたところ、一応抜け道がありました。

JMockitでエラーが起きない実行の仕方

import mockit.Mock;
import mockit.MockUp;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.seasar.framework.unit.Seasar2;
import org.seasar.framework.unit.TestContext;
import test.logic.TestLogic;

@RunWith(Seasar2.class)
public class JMockit2Test {
    private TestContext ctx;
    private IndexAction action;

    public void before() {
        ctx.register(IndexAction.class);
    }

    @Test
    public void テスト() {
        new MockUp<TestLogic>() {
            @Mock
            public String getFirstName() {
                return "jmockit";
            }
        };
        action.index();
        System.out.println(action.firstName);
    }
}

これだとエラーになりません。動きます。
何故かExpectationsやNonStrictExpectationsを使うとエラーになるのに、MockUpだと動くんですよね。。。

このやり方だとメソッド名がリファクタで変更された時に追従されないので、ちょっと面倒ではあります。しかし動いたからこれでいい!・・・のかな。

Mockitoの場合

Mockitoを使ったモックのサンプル

Mocktoは専用のテストランナーの指定がいらないので、以下のコードが動きます。

import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.seasar.framework.unit.Seasar2;
import org.seasar.framework.unit.TestContext;
import test.logic.TestLogic;

@RunWith(Seasar2.class)
public class TestActionMockitoTest {
    private TestContext ctx;
    private IndexAction action;

    public void before() {
        ctx.register(IndexAction.class);
    }

    @Test
    public void テスト() {
        action.testLogic = Mockito.mock(TestLogic.class);
        Mockito.when(action.testLogic.getFirstName()).thenReturn("mockito");
        action.index();
        System.out.println(action.firstName);
    }
}

しかしMockitoには制限があり、制限外の事をすると以下のエラーが発生し、制限事項を教えてくれます。

org.mockito.exceptions.misusing.MissingMethodInvocationException: 
when() requires an argument which has to be 'a method call on a mock'.
For example:
    when(mock.getArticles()).thenReturn(articles);

Also, this error might show up because:
1. you stub either of: final/private/equals()/hashCode() methods.
   Those methods *cannot* be stubbed/verified.
2. inside when() you don't call method on mock but on some other object.
3. the parent of the mocked class is not public.
   It is a limitation of the mock engine.

こうしてみるとそれなりの制限ですね。

雑感

ずっと疑問だったのですが、SaStrutsを現役で使っている方々は、ユニットテストのモックってどうやってるのでしょう・・・?

EasyMockは正直制限が多くて使えないし、PowerMockはSeasar2のテストランナーと共存できないから使えないし、Mockitoは前述の制限があるし、JMockitは前述の謎のエラーが解消できません。

いい感じの解決方法を知っている方がいらっしゃれば、是非教えて下さい!

経験ゼロでもできるプログラミング現場の単体テスト

経験ゼロでもできるプログラミング現場の単体テスト

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)

実践テスト駆動開発 テストに導かれてオブジェクト指向ソフトウェアを育てる (Object Oriented SELECTION)