こんにちはフリューのジョンです。普段はJMockitを使ってテストコードを書いているのですが、今回は、そのJMockitを使ってテストを書いていて、困った2点を書きたいと思います。
この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.
意訳すると、以下のようになります。
JMockitでFileOutputStream(File)をモックするためには、内部でsuperによってFileOutputStream(File, false)を呼び出しているのでそちらをモックしてください。 理由としては、JVMによってsuper(…)またはthis(…)の余分な呼び出しが解消されます。だから、MockUpのproceed()で呼ばれる$initは存在しないコンストラクタを参照してしまうのです。
まとめ
JMockitは複数の書き方ができますが、それ故にわからなくなることも多いと思います。
みんな細かい部分で詰まっていると思いますので、ソースコードを共有していければ解消できるかもしれないですね(切実)