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 {
...
- 階層を表すクラスは、非
static
でpublic
になるようにして、先ほどの共通インナークラス(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
でアノテートしたフィールドを参照したときに、モックとして認識されていない模様。