Edited at

JUnit 4.12 の階層構造と JMockit を組み合わせて使う

More than 3 years have passed since last update.

JUnit 4.12 で、 Enclosed よりも柔軟な階層構造の定義がサポートされるようになりました。

こいつと JMockit を組み合わせて使う方法です。


実装


依存関係


build.gradle

dependencies {

testCompile 'org.jmockit:jmockit:1.19'
testCompile 'junit:junit:4.12'
testCompile 'de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1'
}


テストクラス


SampleTest.java

package sample.junit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.bechte.junit.runners.context.HierarchicalContextRunner;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.Verifications;

// ★runner に HierarchicalContextRunner.class を指定
@RunWith(HierarchicalContextRunner.class)
public class SampleTest {

private static final String UN_USED = null;

// ★static なインナークラスを定義する
public static class TestBase {

protected Sample sample;

// ★共通で利用するモックは、このクラスにまとめて宣言する
@Mocked protected Hoge hoge;
@Mocked protected Fuga fuga;
@Mocked protected Piyo piyo;

@Before
public void setup() {
sample = new Sample();
}
}

// ★TestBase を継承して非 static なインナークラスを定義する
public class サンプルメソッドのテスト extends TestBase {

@Test
public void 渡した値がそのまま返される() throws Exception {
// setup
String arg = "xyz";

// exercise
String actual = sample.method(arg);

// verify
assertThat(actual, is(arg));
}

public class hogeのメソッドがtrueを返す場合 extends TestBase {

@Before
public void setupMock() {
new NonStrictExpectations() {{
hoge.method(); result = true;
}};
}

@Test
public void fugaのメソッドが呼ばれること() throws Exception {
// exercise
sample.method(UN_USED);

// verify
new Verifications() {{
fuga.method(); times = 1;
}};
}
}

public class hogeのメソッドがfalseを返す場合 extends TestBase {

@Test
public void piyoのメソッドが呼ばれること() throws Exception {
// exercise
sample.method(UN_USED);

// verify
new Verifications() {{
piyo.method(); times = 1;
}};
}
}
}
}



各実装


Hoge.java

package sample.junit;

public class Hoge {

public boolean method() {
return false;
}
}



Fuga.java

package sample.junit;

public class Fuga {
public void method() {
}
}



Piyo.java

package sample.junit;

public class Piyo {
public void method() {
}
}



Sample.java

package sample.junit;

public class Sample {

private Hoge hoge = new Hoge();
private Fuga fuga = new Fuga();
private Piyo piyo = new Piyo();

public String method(String arg) {
if (this.hoge.method()) {
this.fuga.method();
} else {
this.piyo.method();
}

return arg;
}
}



ポイント


テストランナーに HierarchicalContextRunner を使用する


SampleTest.java


import de.bechte.junit.runners.context.HierarchicalContextRunner;
...

@RunWith(HierarchicalContextRunner.class)
public class SampleTest {

...



  • 階層構造が使えるようにするためには、テストランナーに HierarchicalContextRunner.class を指定する。


共通利用するモックをインナークラスで定義する


SampleTest.java

    ...

public static class TestBase {

protected Sample sample;

@Mocked protected Hoge hoge;
@Mocked protected Fuga fuga;
@Mocked protected Piyo piyo;

@Before
public void setup() {
sample = new Sample();
}
}

...



  • 共通利用するモックは、インナークラスにまとめて定義する。

  • このクラスには普通テストメソッドを宣言しない。

  • テストメソッドが存在しないインナークラスが存在すると、実行時に JUnit がエラーになる。

  • よって、このインナークラスはテスト対象にならないように宣言しないといけない

  • テスト対象にしないためには、以下のいずれかの条件を満たすようにすればいい。


    • public なクラスにする。


    • static なクラスにする。




階層を表すインナークラス


SampleTest.java

    ...

public class サンプルメソッドのテスト extends TestBase {

...



  • 階層を表すクラスは、非 staticpublic になるようにして、先ほどの共通インナークラス(TestBase)を継承して作成する。


ダメパターン

最初に試してダメだったパターン。

共通利用するモックを、インナークラスではなく SampleTest のフィールドとして定義する。


SampleTest.java

package sample.junit;

import static org.hamcrest.CoreMatchers.*;
import static org.junit.Assert.*;

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;

import de.bechte.junit.runners.context.HierarchicalContextRunner;
import mockit.Mocked;
import mockit.NonStrictExpectations;
import mockit.Verifications;

@RunWith(HierarchicalContextRunner.class)
public class SampleTest {

private static final String UN_USED = null;

private Sample sample;

@Mocked private Hoge hoge;
@Mocked private Fuga fuga;
@Mocked private Piyo piyo;

@Before
public void setup() {
sample = new Sample();
}

public class サンプルメソッドのテスト {

...

public class hogeのメソッドがtrueを返す場合 {

@Before
public void setupMock() {
new NonStrictExpectations() {{
hoge.method(); result = true;
}};
}

@Test
public void fugaのメソッドが呼ばれること() throws Exception {
// exercise
sample.method(UN_USED);

// verify
new Verifications() {{
fuga.method(); times = 1;
}};
}
}

...
}
}



実行結果

java.lang.IllegalStateException: Missing invocation to mocked type at this point; please make sure such invocations appear only after the declaration of a suitable mock field or parameter

at sample.junit.SampleTest$サンプルメソッドのテスト$hogeのメソッドがtrueを返す場合$1.<init>(SampleTest.java:51)
at sample.junit.SampleTest$サンプルメソッドのテスト$hogeのメソッドがtrueを返す場合.setupMock(SampleTest.java:50)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
...

インナークラスで @Mocked でアノテートしたフィールドを参照したときに、モックとして認識されていない模様。


参考