Dagger2を使ったらUnitTestが楽になるか考えてみた

### ※テストコードに関して、新しい記事を投稿しました。
　→　「[Androidでテスト駆動開発に挑戦してみた](https://qiita.com/tak-wisteria/items/b09e208415ac317e425b)」

# 何故、Dagger2なのか？
入り口は直感でした。

「Unit Test をもっと簡単にできないだろうか。DIする設計なら、テストの時にモック注入とかできて楽なんじゃないか。Dagger2って有名だし使ってみるか。」

というざっくりとした直感です。

この記事では、上記を深掘りし、どうやったら Unit Test が楽になるか考えていきます。
※Dagger2の他にも、素敵なフレームワークは沢山あると思います。あくまでご参考まで。

## Q. Unit Test が辛い理由

#### A. 単純に作業量が増える。

私たちは、アプリの仕様やビジネスに応じて、日々、最適な設計を模索しています。そこにテストの要素も追加するのは辛いですよね。


## Q. どうすれば設計に集中できる？

#### A. テストの事なんか考えなくて良いようにする。

最適な設計の検討にパワー全振りしても、テストし易いプログラム構造になってくれる様な仕組みを作る。

「テストが書き辛い」とは、例えば、「テスト対象のロジックに到達する前に、周辺モジュールがヌルポで落ちまくって挫折」という状況。

もし、簡単・柔軟にモック・スタブ等に出来るならば、設計に集中できます！（仮説）


## Q. 簡単・柔軟なモックとは？

#### A. 任意の変数やメソッドをモックにできること。

DaggerはDIする時に@Injectを変数に付けるので、テストコードは@Injectの付いた変数をMockitoでモック化します。


# ソースコード

本題のソースコードです。ここでは以下の環境を用います。

## 使用するライブラリ

・dagger2 : 2.17
・mockito-core:2.23.0
・mockito-android:2.22.0
・junit : 4.12
・robolectric : 3.8
・espresso : 3.0.2


## Dagger関連の実装

なるべくシンプルで分かりやすくなるよう、SubComponentは使いません。

```java:AppComponent
@Component(modules = {AppModule.class})
public interface AppComponent extends IComponent {
    @Component.Builder
    interface AppBuilder {
        @BindsInstance
        AppBuilder application(Application application);
        AppComponent build();
    }
}
```
```java:AppModule
@Module
public class AppModule {

    @Provides
    Context provideContext(Application application) {
        return application;
    }

    @Provides
    public SampleEntity provideSampleEntity() {
        return XXXXX;
    }
}
```

以下のIComponentが、実は肥大化する恐れがあります。
が、プログラムの階層をなるべく平坦にするため、諦めました。

```java:IComponent
/**
 * DIを使う対象のクラスの分だけinjectメソッドを定義してください。
 * abstract classや、genericsクラスは、Dagger2はDIできないので、
 * インナークラスを定義し、そちらにDIしてください。
 */
public interface IComponent {
    void inject(MainActivity activity);
    ...
    void inject(SampleFragment fragment);
    ...
    void inject(AbstractPresenter.DIWrapper wrapper);
    void inject(GenericsPresenter.DIWrapper wrapper);
    ...
}
```

DI class は、DIを簡単に使えるようにしたUtilityです。

```java:DI
public class DI {

    private static AppComponent sAppComponent;

    public static void setAppComponent(AppComponent component) {
        sAppComponent = component;
    }

    public static AppComponent injector() {
        return sAppComponent;
    }
}
```

### DIを使う側の実装
```java:MyApplication
public class MyApplication extends Application {
    @Override
    public void onCreate() {
        super.onCreate();
        DI.setAppComponent(
                DaggerAppComponent
                        .builder()
                        .application(this)
                        .build());
    }
}
```
```java:MainPresenter
public class MainPresenter {
    @Inject MainPresenter(){}
}
```
```java:MainActivity
public class MainActivity extends AppCompatActivity {

    @Inject Context mContext;
    @Inject MainPresenter mPresenter;
    @Inject SampleEntity mEntity;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        DI.injector().inject(this);
        super.onCreate(savedInstanceState);
    }
}
```

## Unit Test の実装

### DIをhookする

DIMock class は、EspressoでもRobolectricでも利用できます。
AppComponentをMockitoでモック化し、injectをhookします。
hookした際、本物のinjectを実行した後、テストロジックにコールバックします。
あとは、使いやすいように幾つかUtility的なメソッドを用意してみました。

```java:DIMock
public class DIMock {

    public interface DICallback<T> {
        void onInject(T listener);
    }

    // original component
    private static AppComponent sOriginal = DI.injector();

    /**
     * injectをhook{@link Mockito#doAnswer(Answer)}するモック
     */
    private static AppComponent sMock;

    private static HashMap<Class<?>, Object> sCustomMocks = new HashMap<>();

    private static HashMap<Class<?>, ArrayList<DICallback>> sCallbacks = new HashMap<>();

    /**
     * DaggerのDIの監視を開始する。
     * DIが実行された後、モックに置き換えたい場合は{@link DIMock#registDICallback(Class, DICallback)} でコールバックを受け取り
     * 個別にfieldに代入する、とか、{@link DIMock#replaceMockAll(Object)}を実行すること。
     */
    static {

        // injectをhook(Mockito.doAnswer)する為の設定をする
        // AppComponentの実態クラスは、Daggerによりfinal宣言されているのでspyできない。
        sMock = Mockito.mock(AppComponent.class);

        Answer answer = invocation -> {

            // injectが呼ばれた際に本コールバックが発生する

            Object arg = invocation.getArgument(0);

            // originalのinjectを実行する
            callInject(sOriginal, arg.getClass(), arg);

            // テスト対象のクラスにコールバックする
            ArrayList<DICallback> list = sCallbacks.get(arg.getClass());
            if (list != null) {
                for (DICallback callback : list) {
                    callback.onInject(arg);
                }
            }

            return null;
        };

        // 全injectをhookする
        for (Method method : IComponent.class.getMethods()) {
            // このタイミングでcallしても実際のメソッドが呼ばれる訳ではない。
            // Mockitoに登録している。
            AppComponent hook = Mockito.doAnswer(answer).when(sMock);
            Class<?> paramType = method.getParameterTypes()[0];
            callInject(hook, paramType, ArgumentMatchers.any(paramType));
        }

        DI.setAppComponent(sMock);
    }

    /**
     * injectを実行する
     */
    private static <T> void callInject(AppComponent component, Class<T> clazz, Object arg) {
        try {
            component.getClass().getMethod("inject", clazz).invoke(component, arg);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /**
     * テストしたいクラスのinjectをhookするコールバックを登録する
     * @param clazz テスト対象のクラス
     * @param callback コールバック
     */
    public static <T> void registDICallback(Class<T> clazz, DICallback<T> callback) {

        ArrayList<DICallback> list = sCallbacks.get(clazz);
        if (list == null) {
            list = new ArrayList<>();
            sCallbacks.put(clazz, list);
        }

        list.add(callback);
    }

    /**
     * Inject対象のインスタンスのインジェクト済みインスタンスを全て{@link Mockito#mock(Class)}で置き換える
     * {@link DIMock#registCustomMock(Class, Object)}で登録済みの場合は、そちらを優先する。
     * @param target
     */
    public static void replaceMockAll(Object target) {
        for(Field field : target.getClass().getDeclaredFields()) {
            if(field.getAnnotation(Inject.class) == null) {
                continue;
            }
            boolean accessible = field.isAccessible();
            try {
                field.setAccessible(true);
                Class<?> type = field.getType();
                Object mock = sCustomMocks.get(type);
                if (mock == null) {
                    mock = Mockito.mock(type);
                }
                field.set(target, mock);
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                field.setAccessible(accessible);
            }
        }
    }

    /**
     * モックをカスタムする場合は、事前に登録する
     * @param clazz mock化するクラス型
     * @param mock mockのインスタンス。{@link Mockito#mock(Class)}を使って作成しても良いし
     *             テスト用スタブ（自作）を指定してもよい。
     */
    public static void registCustomMock(Class clazz, Object mock) {
        sCustomMocks.put(clazz, mock);
    }
}
```

# Unit Test は、楽になるか？

ようやく準備が整いました。実際にテストコードを書いてみます。

## Espressoのテスト

DIMockを使って以下の事をやってみます。
 (1) 特定のクラス型に対して、カスタムのモックオブジェクトを登録する。
 (2) DIが実行されたタイミングで、DI対象のインスタンスを全てモック化する。
 (3) DIが実行されたタイミングで、特定のフィールドのみモック化する。

FragmentだとかViewModelだとか関係なく、DIをhookできます。

以下の例では、MainActivityに対してのinjectをhookし
特定のモジュールを丸ごとモック化したり、一部のメソッドをモック化するサンプルを作成しました。
MainActivityは、SampleFragmentを内包する想定で読んでください。
SampleFragmentもDIを使っているものとします。


```java:Espressoのテスト

    private ActivityTestRule<MainActivity> mRule = new ActivityTestRule<>(MainActivity.class, false, false);

    @Test
    public void myTest() {

        Intent intent = new Intent(); // intentの初期化は省略

        // 事前にカスタムモックを登録しておく ・・・ (1)
        DIMock.registCustomMock(SampleEntity.class, new SampleEntityStub());

        DIMock.registDICallback(MainActivity.class, activity -> {
            // DI対象を全てモックに置き換える ・・・ (2)
            // さっき、registCustomMockしたスタブもこのタイミングで置き換えられる ・・・ (1)
            DIMock.replaceMockAll(activity);
        });

        DIMock.registDICallback(SampleFragment.class, fragment -> {
            // 一部のフィールドだけ個別にモックに置き換えることが出来る ・・・ (3)
            fragment.xxxxx = mock(Xxxxx.class);

            // spyして一部のメソッドを改造する事ができる ・・・ (3)
            SampleFragment spyFragment = spy(fragment);
            Yyyyy spyY = spy(fragment.yyyyy);
        });

        mRule.launchActivity(intent);
        MainActivity activity = mRule.getActivity();
        activity.myLogic();
    }
}
```

## Robolectricのテスト

Espressoの場合と全く同じ流れで同じ事ができます。

```java:Robolectricのテスト

    @Test
    void myTest() {
        DIMock.registDICallback(SampleFragment.class, fragment -> {
            // （Espressoと同じなので略）
        });

        MainActivity activity = Robolectric
                .buildActivity(MainActivity.class)
                .create().get();
        activity.myLogic();
    }
}
```

# いかがでしたでしょうか

楽に柔軟にモック化できるようになりました。
これならガンガンソースを書いていっても、テストし易いのではないでしょうか。（勿論、例外は無限にあると思います。。）

Dagger2 を導入すべきかは、ご自身の状況に留意し、自己責任でご検討くださいね。

ご参考になれば幸いです。