JMockitを使ってテストを書いていて困った点


目次

こんにちはフリューのジョンです。普段はJMockitを使ってテストコードを書いているのですが、今回は、そのJMockitを使ってテストを書いていて、困った2点を書きたいと思います。

  1. すでに@Injectableで書いてあるクラスをモックしたい
  2. FileOutputStreamをJMockitでMockUpを使ってコンストラクタをモックする

この2つです。例えば以下のようなメソッドをテストした場合はどうすればよいのかを書きます。

public class Service {
    @Resource
    private File resourceRootDir;

    @Override
    public void testMethod(byte[] file, String path) {
        // なんかしらの処理があって
        try (OutputStream outputStream = new FileOutputStream(new File(resourceRootDir, path))){
            IOUtils.write(file, outputStream);
        } catch (IOException e) {
            throw new RuntimeException("ファイルを保存できませんでした orz", e);
        }
    }
}

環境

JDK 1.8.0_65
JMockit 1.18
JUnit 4.12

解説

public class ServiceImplTest {
    @Tested
    ServiceImpl tester;

    @Injectable
    File resourceRootDir;

    @Injectable
    File dest;

    @Test
    public void testMethod() throws Exception {
        byte[] fileValue = new byte[100];
        String fileName = "hoge.png";
        new MockUp<IOUtils>() {
            @Mock
            public void write(byte[] data, OutputStream output) throws IOException {
              //ここでアサーションが何かしらある
            }
        };

        new Expectations(File.class) {{//①
            new File(resourceRootDir, "hoge.png"); result = dest;
        }};

        new MockUp<FileOutputStream>() {//②
            @Mock
            public void $init(File file, boolean b) throws FileNotFoundException {
              //ここでアサーションが何かしらある
            }
            @Mock(invocations = 1)
            public void close() throws IOException {
            }
        };

        tester.testMethod(fileValue, fileName);

        new FullVerifications(){{}};
    }
}

1. すでに@Injectableで書いてあるクラスをモックしたい

これは、テスト対象のクラスに@Resourceがあるため、@Injectableを書いているため起きます。

@Mockedを引数に書いてつかうこと、MockUpを使ってやることもできません。(すでにモックされています、というエラーが出ます)

対応としては、ソースコードの①にある通り、Expectationsにクラスを渡すことで、Fileのモックを対象のExpectations内で書くことができます。

今回はコンストラクタをモックしたかったので、返り値となるFileのモックオブジェクトを@Injectableで定義してExpectations内で使用しています。

2. FileOutputStreamのコンストラクタをモックする

これはFileOutputStreamに@Mockedをつけてモックすることができないために起きます。(@Mockedは使えません、代わりに@Injectableや部分モックを使ってくださいというエラーが出ます)

対応しては、ソースコードの②にある通り、MockUpで記述します。

closeメソッドもモックしているのはtry-with-resource文にて自動的にcloseメソッドが呼ばれるためです。(もちろん無いとテストが落ちます)

ここで気をつけることが、一点あります。

実際のコードでは、ファイルを引数としたコンストラクタを使用していますが、テストコードでは異なるコンストラクタをモックします。理由としては以下になります。

To mock the target “FileOutputStream(File)” constructor, JMockit must replace the internal call to another constructor (“this(file, false);”) with an innocuous “super(…)” call. This is because, inside a constructor, the JVM forbids extra calls before a “super(…)” or “this(…)” call is complete. So, in this case calling “proceed()” from $init has no effect since there is no remaining code in the original constructor.

https://groups.google.com/forum/#!topic/jmockit-users/EvTuJKTecDk

意訳すると、以下のようになります。

JMockitでFileOutputStream(File)をモックするためには、内部でsuperによってFileOutputStream(File, false)を呼び出しているのでそちらをモックしてください。 理由としては、JVMによってsuper(…)またはthis(…)の余分な呼び出しが解消されます。だから、MockUpのproceed()で呼ばれる$initは存在しないコンストラクタを参照してしまうのです。

まとめ

JMockitは複数の書き方ができますが、それ故にわからなくなることも多いと思います。

みんな細かい部分で詰まっていると思いますので、ソースコードを共有していければ解消できるかもしれないですね(切実)