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

Source: https://habr.com/ru/post/J277343/


All Articles