Androidアプリを段階的に構築する、パート2





記事の最初の部分では 、MVPパターンを使用してレイヤーで区切られた2つの画面で構成されるgithubを操作するためのアプリケーションを開発しました。 RxJavaを使用して、サーバーと異なるレイヤーの2つのデータモデルとの対話を簡素化しました。 第2部では、Dagger 2を紹介し、ユニットテストを記述し、MockWebServer、JaCoCo、およびRobolectricを見ていきます。



内容:





はじめに



記事の最初の部分では、githubを2段階で操作するための簡単なアプリケーションを作成しました。



条件付き適用スキーム




クラス図




すべてのソースコードはGithubにあります。 リポジトリ内のブランチは、記事のステップに対応しています。 ステップ3依存性注入-3番目のステップ、 ステップ4ユニットテスト-4番目のステップ。



ステップ3.依存性注入



Dagger 2を使用する前に、 Dependency Injection(Dependency Injection)の原理を理解する必要があります。



オブジェクトBを含むオブジェクトAがあるとします。DIを使用しない場合、クラスAのコードでオブジェクトBを作成する必要があります。たとえば、次のようになります。



public class A { B b; public A() { b = new B(); } }
      
      





このようなコードは、 SOLID原則のSRPおよびDRPに直ちに違反します。 最も簡単な解決策は、オブジェクトBをクラスAのコンストラクターに渡すことです。これにより、「手動で」依存性注入を実装します。



 public class A { B b; public A(B b) { this.b = b; } }
      
      





通常、DIはサードパーティのライブラリを使用して実装されます。ここでは、注釈のおかげで、オブジェクトが自動的に置き換えられます。



 public class A { @Inject B b; public A() { inject(); } }
      
      





このメカニズムとAndroidでのそのアプリケーションの詳細については、次の記事をご覧ください。Daggerの例を使用して、依存関係の注入を理解する



ダガー2


Dagger 2は、DIを実装するためにGoogleによって作成されたライブラリです。 コード生成における主な利点、つまり すべてのエラーはコンパイル段階で表示されます。 ハブには、 ダガー2に関する良い記事があります。 公式ページコードパスに関する適切な指示を読むこともできます。



Dagger 2をインストールするには、build.gradleを編集します。



build.gradle
 apply plugin: 'com.android.application' apply plugin: 'com.neenbedankt.android-apt' dependencies { compile fileTree(dir: 'libs', include: ['*.jar']) compile 'com.android.support:appcompat-v7:21.0.3' compile 'com.google.dagger:dagger:2.0-SNAPSHOT' apt 'com.google.dagger:dagger-compiler:2.0-SNAPSHOT' provided 'org.glassfish:javax.annotation:10.0-b28' }
      
      







Dagger IntelliJ Pluginプラグインをインストールすることも強くお勧めします。 注入が発生する場所と場所から移動するのに役立ちます。



Dagger IntelliJプラグイン




Dagger 2は、モジュールメソッドからメソッドを実装するためにオブジェクトを取得します(メソッドには、アノテーションの提供 、モジュール- モジュールでマークする必要があります )か、アノテーション付きInjectクラスのコンストラクターを使用してオブジェクトを作成します。 例:



 @Module public class ModelModule { @Provides @Singleton ApiInterface provideApiInterface() { return ApiModule.getApiInterface(); } }
      
      





または



 public class RepoBranchesMapper @Inject public RepoBranchesMapper() {} }
      
      





埋め込むフィールドは、 Injectアノテーションで示されます。



 @Inject protected ApiInterface apiInterface;
      
      





これら2つのものは、コンポーネント(@Component)を使用して接続されます。 それらは、オブジェクトをどこから取得し、どこに注入するかを示します(メソッドの注入)。 例:



 @Singleton @Component(modules = {ModelModule.class}) public interface AppComponent { void inject(ModelImpl dataRepository); }
      
      





Dagger 2では、1つのコンポーネント(AppComponent)と3つのモジュールを異なるレイヤー(モデル、プレゼンテーション、ビュー)に使用します。



担当者
 @Singleton @Component(modules = {ModelModule.class, PresenterModule.class, ViewModule.class}) public interface AppComponent { void inject(ModelImpl dataRepository); void inject(BasePresenter basePresenter); void inject(RepoListPresenter repoListPresenter); void inject(RepoInfoPresenter repoInfoPresenter); void inject(RepoInfoFragment repoInfoFragment); }
      
      







モデル


モデルレイヤーの場合、フローを管理するためのApiInterfaceと2つのスケジューラを提供する必要があります。 スケジューラの場合、Daggerが依存関係グラフを把握できるように、 名前付き注釈を使用する必要があります。



ModelModule
 @Provides @Singleton ApiInterface provideApiInterface() { return ApiModule.getApiInterface(Const.BASE_URL); } @Provides @Singleton @Named(Const.UI_THREAD) Scheduler provideSchedulerUI() { return AndroidSchedulers.mainThread(); } @Provides @Singleton @Named(Const.IO_THREAD) Scheduler provideSchedulerIO() { return Schedulers.io(); }
      
      







発表者


プレゼンターレイヤーには、ModelとCompositeSubscription、およびマッパーを提供する必要があります。 注釈付きコンストラクターを使用して、モジュール、マッパーを介してModelおよびCompositeSubscriptionを提供します。



プレゼンターモジュール
 public class PresenterModule { @Provides @Singleton Model provideDataRepository() { return new ModelImpl(); } @Provides CompositeSubscription provideCompositeSubscription() { return new CompositeSubscription(); } }
      
      







注釈付きコンストラクターを使用したマッパーの例
 public class RepoBranchesMapper implements Func1<List<BranchDTO>, List<Branch>> { @Inject public RepoBranchesMapper() { } @Override public List<Branch> call(List<BranchDTO> branchDTOs) { List<Branch> branches = Observable.from(branchDTOs) .map(branchDTO -> new Branch(branchDTO.getName())) .toList() .toBlocking() .first(); return branches; } }
      
      







表示する


Viewレイヤーとプレゼンターの紹介により、状況はより複雑になります。 プレゼンターを作成するとき、コンストラクターでViewインターフェイスを渡します。 したがって、Daggerには、このインターフェイスの実装、つまりフラグメントへのリンクが必要です。 別の方法として、プレゼンターインターフェイスを変更し、onCreateにビューリンクを渡すこともできます。 両方のケースを検討します。



ビューリンクを渡します。



RepoListViewインターフェースを実装するRepoListFragmentフラグメントがあり、

RepoListPresenterは、このRepoListViewをコンストラクターへの入力として受け入れます。 RepoListFragmentにRepoListPresenterを実装する必要があります。 このようなスキームを実装するには、コンストラクターでRepoListViewインターフェイスへのリンクを受け入れる新しいコンポーネントと新しいモジュールを作成する必要があります。 このモジュールでは、(RepoListViewインターフェイスへのリンクを使用して)プレゼンターを作成し、フラグメントに埋め込みます。



フラグメントでの注入
 @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); DaggerViewComponent.builder() .viewDynamicModule(new ViewDynamicModule(this)) .build() .inject(this); }
      
      







成分
 @Singleton @Component(modules = {ViewDynamicModule.class}) public interface ViewComponent { void inject(RepoListFragment repoListFragment); }
      
      







モジュール
 @Module public class ViewDynamicModule { RepoListView view; public ViewDynamicModule(RepoListView view) { this.view = view; } @Provides RepoListPresenter provideRepoListPresenter() { return new RepoListPresenter(view); } }
      
      







実際のアプリケーションでは、多くのインジェクションとモジュールがあるため、エンティティごとに異なるコンポーネントを作成することは、 神オブジェクトの作成を防ぐための素晴らしいアイデアです。



発表者コードを変更します。



上記の方法では、いくつかのファイルと多くのアクションを作成する必要があります。 この場合、はるかに簡単な方法があります。コンストラクタを変更し、onCreateのインターフェイスへのリンクを転送します。

コード:



フラグメントでの注入
 @Inject RepoInfoPresenter presenter; @Override public void onCreate(@Nullable Bundle savedInstanceState) { super.onCreate(savedInstanceState); App.getComponent().inject(this); presenter.onCreate(this, getRepositoryVO()); }
      
      







モジュール
 @Module public class ViewModule { @Provides RepoInfoPresenter provideRepoInfoPresenter() { return new RepoInfoPresenter(); } }
      
      







Dagger 2の実装が完了したら、アプリケーションのテストに移りましょう。



ステップ4.ユニットテスト



テストは長い間、ソフトウェア開発プロセスの不可欠な部分でした。

ウィキペディアでは、多くの種類のテストを特定しています 。まず、単体テストを扱います。



単体テストは、プログラムのソースコードの個々のモジュールの正確性を確認できるプログラミングプロセスです。

アイデアは、重要なメソッドごとにテストを作成することです。 これにより、次のコード変更がリグレッションにつながっているかどうか、つまり、プログラムのテスト済みの場所でエラーが発生しているかどうかをすばやく確認でき、そのようなエラーの検出と除去が容易になります。



すべてのコンポーネントが相互に作用するため、完全に分離されたテストを作成することはできません。 単体テストでは、mokamiに囲まれた1つのモジュールの動作をチェックすることを意味します。 統合テストでいくつかの実際のモジュールの相互作用を確認します。



モジュールの相互作用スキーム:







マッパーテストの例(グレーモジュール-未使用、緑-moki、青-テスト中のモジュール):







インフラ



ツールとフレームワークにより、テストの作成とサポートが容易になります。 赤いテストとのマージを防ぐCIサーバーは、masterブランチで予期せずにテストを中断する可能性を劇的に減らします。 テストとナイトリービルドを自動的に実行すると、問題を早期に特定できます。 この原理はフェイルファーストと呼ばれます

テスト環境については、 Androidでのテスト:Robolectric + Jenkins +Jaooの記事をご覧ください 。 将来的には、 Robolecricを使用してテストを作成し、 mockitoを使用してモックを作成し、 Jaooを使用してコードのカバレッジをテストでテストします。



MVPパターンを使用すると、コードのテストを迅速かつ効率的に記述できます。 Dagger 2の助けを借りて、実際のオブジェクトをテストmokiに置き換えて、コードを外部から隔離できます。 このために、テストモジュールを備えたテストコンポーネントを使用します。 コンポーネントは、テストアプリケーションで置き換えられます。テストアプリケーションは、ベーステストクラスのConfigアノテーション(application = TestApplication.class)を使用して設定します。



JaCoCoコードカバレッジ



開始する前に、テストするメソッドとカバレッジテストの割合の計算方法を決定する必要があります。 これを行うには、テスト結果に関するレポートを生成するJaCoCoライブラリを使用します。

最新のAndroid Studio は、すぐに使用できるコードカバレッジをサポートしています。または、build.gradleに次の行を追加して構成できます。



build.gradle
 apply plugin: 'jacoco' jacoco { toolVersion = "0.7.1.201405082137" } def coverageSourceDirs = [ '../app/src/main/java' ] task jacocoTestReport(type: JacocoReport, dependsOn: "testDebugUnitTest") { group = "Reporting" description = "Generate Jacoco coverage reports" classDirectories = fileTree( dir: '../app/build/intermediates/classes/debug', excludes: ['**/R.class', '**/R$*.class', '**/*$ViewInjector*.*', '**/*$ViewBinder*.*', //DI '**/*_MembersInjector*.*', //DI '**/*_Factory*.*', //DI '**/testrx/model/dto/*.*', //dto model '**/testrx/presenter/vo/*.*', //vo model '**/testrx/other/**', '**/BuildConfig.*', '**/Manifest*.*', '**/Lambda$*.class', '**/Lambda.class', '**/*Lambda.class', '**/*Lambda*.class'] ) additionalSourceDirs = files(coverageSourceDirs) sourceDirectories = files(coverageSourceDirs) executionData = files('../app/build/jacoco/testDebugUnitTest.exec') reports { xml.enabled = true html.enabled = true } }
      
      







除外されたクラスに注意してください:Dagger 2とDTOおよびVOモデルに関連するすべてを削除しました。



jacoco(jacocoTestReportを段階的に実行)を実行し、結果を確認します。







これで、テストの数と理想的に一致するカバレッジの割合、つまり0%=)になりました。この状況を修正しましょう。



モデル


モデル層では、レトロフィット(ApiInterface)設定の正確性、クライアント作成の正確性、ModelImplの操作を確認する必要があります。

コンポーネントは分離してスキャンする必要があるため、サーバーをエミュレートする必要があるかどうかを確認するには、 MockWebServerがこれを支援します。 サーバーの応答を構成し、改造要求を確認します。



モデル層スキーム、テストが必要なクラスは赤でマークされています




Dagger 2のテストモジュール
 @Module public class ModelTestModule { @Provides @Singleton ApiInterface provideApiInterface() { return mock(ApiInterface.class); } @Provides @Singleton @Named(Const.UI_THREAD) Scheduler provideSchedulerUI() { return Schedulers.immediate(); } @Provides @Singleton @Named(Const.IO_THREAD) Scheduler provideSchedulerIO() { return Schedulers.immediate(); } }
      
      







試験例
 public class ApiInterfaceTest extends BaseTest { private MockWebServer server; private ApiInterface apiInterface; @Before public void setUp() throws Exception { super.setUp(); server = new MockWebServer(); server.start(); final Dispatcher dispatcher = new Dispatcher() { @Override public MockResponse dispatch(RecordedRequest request) throws InterruptedException { if (request.getPath().equals("/users/" + TestConst.TEST_OWNER + "/repos")) { return new MockResponse().setResponseCode(200) .setBody(testUtils.readString("json/repos")); } else if (request.getPath().equals("/repos/" + TestConst.TEST_OWNER + "/" + TestConst.TEST_REPO + "/branches")) { return new MockResponse().setResponseCode(200) .setBody(testUtils.readString("json/branches")); } else if (request.getPath().equals("/repos/" + TestConst.TEST_OWNER + "/" + TestConst.TEST_REPO + "/contributors")) { return new MockResponse().setResponseCode(200) .setBody(testUtils.readString("json/contributors")); } return new MockResponse().setResponseCode(404); } }; server.setDispatcher(dispatcher); HttpUrl baseUrl = server.url("/"); apiInterface = ApiModule.getApiInterface(baseUrl.toString()); } @Test public void testGetRepositories() throws Exception { TestSubscriber<List<RepositoryDTO>> testSubscriber = new TestSubscriber<>(); apiInterface.getRepositories(TestConst.TEST_OWNER).subscribe(testSubscriber); testSubscriber.assertNoErrors(); testSubscriber.assertValueCount(1); List<RepositoryDTO> actual = testSubscriber.getOnNextEvents().get(0); assertEquals(7, actual.size()); assertEquals("Android-Rate", actual.get(0).getName()); assertEquals("andrey7mel/Android-Rate", actual.get(0).getFullName()); assertEquals(26314692, actual.get(0).getId()); } @After public void tearDown() throws Exception { server.shutdown(); } }
      
      







モデルをテストするために、ApiInterfaceをワイプし、正しい動作を確認します。



ModelImplのサンプルテスト
 @Test public void testGetRepoBranches() { BranchDTO[] branchDTOs = testUtils.getGson().fromJson(testUtils.readString("json/branches"), BranchDTO[].class); when(apiInterface.getBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO)).thenReturn(Observable.just(Arrays.asList(branchDTOs))); TestSubscriber<List<BranchDTO>> testSubscriber = new TestSubscriber<>(); model.getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO).subscribe(testSubscriber); testSubscriber.assertNoErrors(); testSubscriber.assertValueCount(1); List<BranchDTO> actual = testSubscriber.getOnNextEvents().get(0); assertEquals(3, actual.size()); assertEquals("QuickStart", actual.get(0).getName()); assertEquals("94870e23f1cfafe7201bf82985b61188f650b245", actual.get(0).getCommit().getSha()); }
      
      







Jacocoのカバレッジを確認します。







発表者


プレゼンターレイヤーでは、マッパーの作業とプレゼンターの作業をテストする必要があります。



Presenterレイヤーのレイアウト、テストが必要なクラスは赤でマークされています




マッパーでは、すべてが非常に簡単です。 ファイルからjsonを読み取り、変換して確認します。

プレゼンターを使用して、モデルを起動し、ビューに必要なメソッドの呼び出しを確認します。 また、onSubscribeとonStopの正確さを確認する必要があります。これは、サブスクリプションをインターセプトしてisUnsubscribedを確認するためです。



プレゼンターレイヤーでのテスト例
  @Before public void setUp() throws Exception { super.setUp(); component.inject(this); activityCallback = mock(ActivityCallback.class); mockView = mock(RepoListView.class); repoListPresenter = new RepoListPresenter(mockView, activityCallback); doAnswer(invocation -> Observable.just(repositoryDTOs)) .when(model) .getRepoList(TestConst.TEST_OWNER); doAnswer(invocation -> TestConst.TEST_OWNER) .when(mockView) .getUserName(); } @Test public void testLoadData() { repoListPresenter.onCreateView(null); repoListPresenter.onSearchButtonClick(); repoListPresenter.onStop(); verify(mockView).showRepoList(repoList); } @Test public void testSubscribe() { repoListPresenter = spy(new RepoListPresenter(mockView, activityCallback)); //for ArgumentCaptor repoListPresenter.onCreateView(null); repoListPresenter.onSearchButtonClick(); repoListPresenter.onStop(); ArgumentCaptor<Subscription> captor = ArgumentCaptor.forClass(Subscription.class); verify(repoListPresenter).addSubscription(captor.capture()); List<Subscription> subscriptions = captor.getAllValues(); assertEquals(1, subscriptions.size()); assertTrue(subscriptions.get(0).isUnsubscribed()); }
      
      







JaCoCoの変更を参照してください。







表示する


ビューレイヤーをテストするときは、フラグメントからプレゼンターライフサイクルメソッドの呼び出しのみをチェックする必要があります。 すべてのロジックはプレゼンターに含まれています。



レイヤービュー図、テストが必要なクラスは赤でマークされています




フラグメントテストの例
 @Test public void testOnCreateViewWithBundle() { repoInfoFragment.onCreateView(LayoutInflater.from(activity), (ViewGroup) activity.findViewById(R.id.container), bundle); verify(repoInfoPresenter).onCreateView(bundle); } @Test public void testOnStop() { repoInfoFragment.onStop(); verify(repoInfoPresenter).onStop(); } @Test public void testOnSaveInstanceState() { repoInfoFragment.onSaveInstanceState(null); verify(repoInfoPresenter).onSaveInstanceState(null); }
      
      







最終テスト範囲:







結論または継続...



記事の第2部では、Dagger 2の実装を調べ、ユニットコードをテストでカバーしました。 MVPとインジェクションインジェクションのおかげで、アプリケーションのすべての部分のテストをすばやく作成できました。 すべてのコードはgithubで入手できます 。 記事はnnesterovの積極的な参加で書かれました。 次のパートでは、統合テストと機能テスト、およびTDDについて説明します。



更新

Androidアプリを段階的に構築する、パート3



All Articles