110
107

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

現実世界でのAndroid + Dagger2によるテスト

Posted at

はじめに

Dagger2使ってますか?
小規模なアプリであれば必要ないですが、ある程度規模が大きかったり、複数チーム (特に複数社) で開発する場合は便利ですよね。

Android + Dagger2でテストする日本語の説明があまりない気がするので、書いてみます。
Dagger2の基本的な使い方は端折りますので、適当に検索してください。

環境

本記事の本質とは関係ありませんが、コードを簡単にするためにButter KnifeLombokも使ってます。

出来たもの

ソース一式を以下に置いておきます。
以降、これベースで説明していきます。
https://github.com/arenahito/AndroidDiExample

構造

サンプルアプリの構造としては、Android-CleanArchitectureをベースとしたPresentation - Domain - Dataの3レイヤー構造です。Presentation - Domain間はUseCase、Domain - Data間はRepositoryで接続し、実体はDagger2でインジェクトします。
本来であればData Storeから外部システムに接続しますが、サンプルではインメモリのリストを使ってます。

Layers Structure.png

DI

基本構造

DI用のクラスはPresentationレイヤのdiパッケージに配置します。
構造的には下図のような感じ。

Dagger2のコンポーネントは、Sub Componentによる階層構造とします。
複数のComponentはDependencyで関連づけることも出来ますが、DependencyだとテストしづらいのでSub Componentです。

DI Structure.png

Gradle

まずはGradleの設定です。
テストコードでもDagger2を使うので、androidTestAptの設定も忘れずに。

build.gradle
compile 'com.google.dagger:dagger:2.5'
apt 'com.google.dagger:dagger-compiler:2.5'
androidTestApt 'com.google.dagger:dagger-compiler:2.5'

Component

ApplicationComponentnはApplicaitonクラス、ActivityComponentはActivityの基底クラス、XxxActivityComponentは実際のActivityクラスに対応します。
Serviceを作る場合は、ActivityComponentと同じレベルでServiceComponentを作ります。

実際のコードはこんな感じ。

ApplicationComponent.java
@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {

    ActivityComponent activityComponent();

    // サンプルで利用していませんが、よく使いそうなので。
    Context context();

    TasksRepository tasksRepository();

}
ActivityComponent.java
@Subcomponent(modules = ActivityModule.class)
@PerActivity
public interface ActivityComponent {

    MainActivityComponent mainActivityComponent();

}
MainActivityComponent.java
@Subcomponent(modules = MainActivityModule.class)
@PerActivity
public interface MainActivityComponent {

    void inject(MainActivityFragment mainActivityFragment);

}

Module

XxxComponentに対応したXxxModuleを作ります。

ApplicationModule.java
@Module
public class ApplicationModule {

    private Application application;

    public ApplicationModule(Application application) {
        this.application= application;
    }

    @Provides
    @Singleton
    public Context provideContext() {
        return application;
    }

    @Provides
    @Singleton
    public TasksRepository provideTasksRepository() {
        return new TasksRepositoryImpl();
    }


}
ActivityModule.java
@Module
public class ActivityModule {

}
MainActivityModule.java
@Module
public class MainActivityModule {

    @Provides
    public TasksUseCase provideTasksUseCase(TasksRepository repository) {
        return new TasksUseCaseImpl(repository);
    }

}

インジェクト

Viewなどで実際にインジェクトするには、下記のようにします。
実際には、ApplicationとかActivityで自分用のコンポーネントを保持、元ネタのコンポーネントは親から取得すれば良いです。

DaggerApplicationComponent
        .builder()
        .applicationModule(new ApplicationModule(this))
        .build()
        .activityComponent()
        .mainActivityComponent()
        .inject(this);

Application

ApplicationクラスはApplication Componentを返すようにするわけですが、これを外から変更できるようにします。
テストの時はApplication Componentをテスト用に置き換えるわけです。

DiExampleApplication.java
public class DiExampleApplication extends Application {

    private ApplicationComponent component;

    @Override
    public void onCreate() {
        super.onCreate();
        initializeComponent();
    }

    private void initializeComponent() {
        component = DaggerApplicationComponent
                .builder()
                .applicationModule(new ApplicationModule(this))
                .build();
    }

    public void setComponent(ApplicationComponent component) {
        this.component = component;
    }

    public ApplicationComponent getApplicationComponent() {
        return component;
    }

}

テスト

構造

本記事の主題であるテストのお話しです。
本物のComponent、Moduleに対応するTestComponent、TestModuleを用意して、Activity起動前にApplicationが保有するApplication Componentを置き換えます。

単純に本物のApplication Componentと同じようにTestComponent、TestModuleを作っていくと、テストデータのパターン数だけTestModuleクラスが必要となってしまいます。そこで、大元のTestApplicationComponentでTestXxxActivityModuleを設定できるようにします。TestApplicationComponentの初期化時にテストデータを設定したTestXxxActivityModuleを設定することにより、テストデータを変更できるようにします。

 DI Test Structure.png

テストコード

実際のコードはこんな感じ。
TestApplicationに対してTestMainActivityModuleを設定すると、それをTestMainActivityComponentが参照して良い感じにしてくれます。

Sub Componentを使うことによって、Application Componentを変更することで全コンポーネントを変更することが出来ます。
DependencyだとCompoentの生成がいたるところに散らばるため、こう簡単にはできないです。

MainActivityTest.java
@RunWith(AndroidJUnit4.class)
public class MainActivityTest {

    @Rule
    public ActivityTestRule<MainActivity> activityRule = new ActivityTestRule<>(MainActivity.class);

    @Test
    public void testInitialTasks() {
        TasksUseCase testTasksUseCase = new TasksUseCase() {
            @Override
            public List<Task> findAll() {
                return new ArrayList<>(Arrays.asList(
                        new Task(0, "Test001"),
                        new Task(1, "Test002"),
                        new Task(2, "Test003")
                ));
            }

            @Override
            public void save(Task task) {

            }

            @Override
            public void delete(int id) {

            }
        };

        getApplication().setComponent(
                DaggerMainActivityTest_TestApplicationComponent
                        .builder()
                        .applicationModule(new ApplicationModule(getApplication()))
                        .testMainActivityModule(new TestMainActivityModule(testTasksUseCase))
                        .build()
        );

        activityRule.launchActivity(new Intent());

        DataInteraction taskListView = onData(anything()).inAdapterView(withId(R.id.taskListView));
        taskListView.atPosition(0).check(matches(withText("id=0, text=Test001")));
        taskListView.atPosition(1).check(matches(withText("id=1, text=Test002")));
        taskListView.atPosition(2).check(matches(withText("id=2, text=Test003")));
    }

    private DiExampleApplication getApplication() {
        return (DiExampleApplication) InstrumentationRegistry
                .getInstrumentation()
                .getTargetContext()
                .getApplicationContext();
    }

    @Component(modules = {ApplicationModule.class, TestMainActivityModule.class})
    @Singleton
    public interface TestApplicationComponent extends ApplicationComponent {

        @Override
        TestActivityComponent activityComponent();

    }

    @Subcomponent(modules = ActivityModule.class)
    @PerActivity
    public interface TestActivityComponent extends ActivityComponent {

        @Override
        TestMainActivityComponent mainActivityComponent();

    }

    @Subcomponent(modules = TestMainActivityModule.class)
    @PerActivity
    public interface TestMainActivityComponent extends MainActivityComponent {

        void inject(MainActivityFragment mainActivityFragment);

    }

    @Module
    public class TestMainActivityModule {

        private TasksUseCase tasksUseCase;

        public TestMainActivityModule(TasksUseCase tasksUseCase) {
            this.tasksUseCase = tasksUseCase;
        }

        @Provides
        public TasksUseCase provideTasksUseCase() {
            return tasksUseCase;
        }

    }

}

最後に

サーバアプリからAndroidアプリに移った方にとっては、DIは当たり前の技術であり比較できすんなりと受け入れられるのではないかと思います。ですが、いきなりAndroidアプリ開発に入った方には受け入れづらいかもしれません。

きっちりレイヤを切ることにより、他レイヤの実装を意識せずにテストすることが出来ますので、テストコードもシンプルで書きやすくなります。
次の新規開発の際には、検討してみてはいかがでしょうか。

110
107
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
110
107

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?