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

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

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

# 実装
## 依存関係
```groovy:build.gradle
dependencies {
    testCompile 'org.jmockit:jmockit:1.19'
    testCompile 'junit:junit:4.12'
    testCompile 'de.bechte.junit:junit-hierarchicalcontextrunner:4.12.1'
}
```

## テストクラス
```java: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;
                }};
            }
        }
    }
}
```

## 各実装
```java:Hoge.java
package sample.junit;

public class Hoge {

    public boolean method() {
        return false;
    }
}
```

```java:Fuga.java
package sample.junit;

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

```java:Piyo.java
package sample.junit;

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

```java: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` を使用する
```java:SampleTest.java

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

@RunWith(HierarchicalContextRunner.class)
public class SampleTest {

...
```

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

## 共通利用するモックをインナークラスで定義する
```java: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` なクラスにする。

## 階層を表すインナークラス
```java:SampleTest.java
    ...

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

- 階層を表すクラスは、非 `static` で `public` になるようにして、先ほどの共通インナークラス（`TestBase`）を継承して作成する。

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

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

```java: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;
                }};
            }
        }
        
        ...
    }
}
```

```text:実行結果
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` でアノテートしたフィールドを参照したときに、モックとして認識されていない模様。

# 参考
- [JUnitのテストの階層化と@InjectMocks - Qiita](http://qiita.com/n_slender/items/6b7e4af5f79e2bd4f921)