はじめに
Dagger2使ってますか?
小規模なアプリであれば必要ないですが、ある程度規模が大きかったり、複数チーム (特に複数社) で開発する場合は便利ですよね。
Android + Dagger2でテストする日本語の説明があまりない気がするので、書いてみます。
Dagger2の基本的な使い方は端折りますので、適当に検索してください。
環境
本記事の本質とは関係ありませんが、コードを簡単にするためにButter KnifeとLombokも使ってます。
- Dagger2 2.5
- Butter Knife 8.1.0
- Lombok 1.12.6
出来たもの
ソース一式を以下に置いておきます。
以降、これベースで説明していきます。
https://github.com/arenahito/AndroidDiExample
構造
サンプルアプリの構造としては、Android-CleanArchitectureをベースとしたPresentation - Domain - Dataの3レイヤー構造です。Presentation - Domain間はUseCase、Domain - Data間はRepositoryで接続し、実体はDagger2でインジェクトします。
本来であればData Storeから外部システムに接続しますが、サンプルではインメモリのリストを使ってます。
DI
基本構造
DI用のクラスはPresentationレイヤのdiパッケージに配置します。
構造的には下図のような感じ。
Dagger2のコンポーネントは、Sub Componentによる階層構造とします。
複数のComponentはDependencyで関連づけることも出来ますが、DependencyだとテストしづらいのでSub Componentです。
Gradle
まずはGradleの設定です。
テストコードでもDagger2を使うので、androidTestAptの設定も忘れずに。
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を作ります。
実際のコードはこんな感じ。
@Component(modules = ApplicationModule.class)
@Singleton
public interface ApplicationComponent {
ActivityComponent activityComponent();
// サンプルで利用していませんが、よく使いそうなので。
Context context();
TasksRepository tasksRepository();
}
@Subcomponent(modules = ActivityModule.class)
@PerActivity
public interface ActivityComponent {
MainActivityComponent mainActivityComponent();
}
@Subcomponent(modules = MainActivityModule.class)
@PerActivity
public interface MainActivityComponent {
void inject(MainActivityFragment mainActivityFragment);
}
Module
XxxComponentに対応したXxxModuleを作ります。
@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();
}
}
@Module
public class ActivityModule {
}
@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をテスト用に置き換えるわけです。
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を設定することにより、テストデータを変更できるようにします。
テストコード
実際のコードはこんな感じ。
TestApplicationに対してTestMainActivityModuleを設定すると、それをTestMainActivityComponentが参照して良い感じにしてくれます。
Sub Componentを使うことによって、Application Componentを変更することで全コンポーネントを変更することが出来ます。
DependencyだとCompoentの生成がいたるところに散らばるため、こう簡単にはできないです。
@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アプリ開発に入った方には受け入れづらいかもしれません。
きっちりレイヤを切ることにより、他レイヤの実装を意識せずにテストすることが出来ますので、テストコードもシンプルで書きやすくなります。
次の新規開発の際には、検討してみてはいかがでしょうか。