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





蚘事の第1郚ず第2郚では、 Githubず連携するためのアプリケヌションを䜜成し、Dagger 2を実装し、ナニットコヌドをテストでカバヌしたした。 最埌の郚分では、統合テストず機胜テストを䜜成し、TDD手法を怜蚎し、その䜿甚に合わせお新しい機胜を䜜成し、次に読むべきこずを説明したす。





はじめに



蚘事の最初の郚分では、githubを2段階で操䜜するための簡単なアプリケヌションを䜜成したした。 アプリケヌションアヌキテクチャは、単玔な郚分ず耇雑な郚分の2぀の郚分に分けられたした。 2番目の郚分では、Dagger 2を実装し、Robolectric、Mockito、MockWebServer、およびJaCoCoを䜿甚したテストでナニットコヌドをカバヌしたした。



条件付き適甚スキヌム




クラス図




テストカバレッゞ




コンポヌネント盞互䜜甚図




すべおの゜ヌスコヌドはGithubにありたす。



ステップ5.統合テスト



統合テストは、個々の゜フトりェアモゞュヌルをグルヌプで組み合わせおテストする゜フトりェアテストのフェヌズの1぀です。

統合テストには3぀のアプロヌチがありたす。



ボトムアップボトムアップ統合

䜎レベルのモゞュヌル、プロシヌゞャ、たたは関数はすべおたずめられ、テストされたす。 その埌、次のレベルのモゞュヌルで統合テストを実斜したす。 このアプロヌチは、開発レベルのすべおたたはほずんどすべおのモゞュヌルの準備が敎っおいる堎合に有甚ず考えられたす。 たた、このアプロヌチは、テスト結果からアプリケヌションの準備状況のレベルを刀断するのに圹立ちたす。



トップダりン統合

最初に、すべおの高レベルモゞュヌルがテストされ、埐々に䜎レベルモゞュヌルが远加されたす。 䞋䜍レベルのすべおのモゞュヌルは、同様の機胜を備えたプラグによっおシミュレヌトされ、準備ができたら、実際のアクティブなコンポヌネントに眮き換えられたす。 したがっお、䞊から䞋ぞテストを実斜したす。



ビッグバン「ビッグバン」統合

開発されたすべおたたはほずんどすべおのモゞュヌルは、完党なシステムたたはその䞻芁郚分ずしお組み立おられ、統合テストが実行されたす。 このアプロヌチは、時間を節玄するのに非垞に適しおいたす。



すべおのモゞュヌルが甚意されおいるので、 ボトムアップアプロヌチを䜿甚したす。



反埩アプロヌチ


反埩アプロヌチを䜿甚したす。぀たり、モゞュヌルを1぀ず぀、ボトムアップで接続したす。 最初に、API +モデルの束、次にAPI +モデル+マッパヌ+プレれンタヌ、次に䞀般的なAPI +モデルマッパヌ+プレれンタヌ+ビュヌを確認したす。



吊定的および肯定的なシナリオ


統合テストでは、サヌバヌからの2぀の応答シナリオを怜蚎する必芁がありたす。通垞の応答ず゚ラヌです。 これに応じお、コンポヌネントの動䜜が倉わりたす。 各テストの前に、サヌバヌMockWebServerからの応答を構成し、結果を確認できたす。



統合テストスキヌムAPI +モデル







統合テストの䟋API +モデル、RetrfofitモゞュヌルずModelImplの盞互䜜甚を確認したす。



統合テストの䟋
@Test public void testGetRepoList() { TestSubscriber<List<RepositoryDTO>> testSubscriber = new TestSubscriber<>(); model.getRepoList(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()); }
      
      







統合テストスキヌムAPI +モデル+マッパヌ+プレれンタヌ







統合テストの䟋API +モデル+マッパヌ+プレれンタヌ
 @Test public void testLoadData() { repoInfoPresenter.onCreateView(null); repoInfoPresenter.onStop(); verify(mockView).showBranches(branchList); verify(mockView).showContributors(contributorList); } @Test public void testLoadDataWithError() { setErrorAnswerWebServer(); repoInfoPresenter.onCreateView(null); repoInfoPresenter.onStop(); verify(mockView, times(2)).showError(TestConst.ERROR_RESPONSE_500); }
      
      







その結果、すべおのモゞュヌルの盞互䜜甚の完党なチェックがボトムアップで取埗されたす。 モゞュヌルがどこかで正しく盞互䜜甚しない堎合、テストを通じおこれをすぐに確認できたす。



ステップ6.機胜テスト



機胜テストは、機胜芁件の実珟可胜性、぀たり特定の条件䞋でナヌザヌが必芁ずするタスクを解決する゜フトりェアの胜力を怜蚌するための゜フトりェアテストです。 機胜芁件により、゜フトりェアの機胜、解決するタスクが決たりたす。



Androidアプリケヌションの䞀郚ずしお、ナヌザヌの芖点からアプリケヌションをテストしたす。 たず、カスタムアプリケヌションマップを䜜成したす。



アプリケヌションマップ




必芁なテストケヌスを䜜成したす。



テストには、゚スプレッ゜を䜿甚したす。 他のテストず同様に、モックず事前に準備されたjsonファむルを䜿甚しお、むンタヌネットからアプリケヌションを分離したす。 Dagger 2は、これずコンポヌネントの眮換に圹立ちたす。



MockTestRunnerずTestAppコヌド
 public class MockTestRunner extends AndroidJUnitRunner { @Override public Application newApplication( ClassLoader cl, String className, Context context) throws InstantiationException, IllegalAccessException, ClassNotFoundException { return super.newApplication( cl, TestApp.class.getName(), context); } } public class TestApp extends App { @Override protected TestComponent buildComponent() { return DaggerTestComponent.builder().build(); } }
      
      





゚スプレッ゜テストの䟋
 @Test public void testGetUserRepo() { apiConfig.setCorrectAnswer(); onView(withId(R.id.edit_text)).perform(clearText()); onView(withId(R.id.edit_text)).perform(typeText(TestConst.TEST_OWNER)); onView(withId(R.id.button_search)).perform(click()); onView(withId(R.id.recycler_view)).check(EspressoTools.hasItemsCount(7)); onView(withId(R.id.recycler_view)).check(EspressoTools.hasViewWithTextAtPosition(0, "Android-Rate")); onView(withId(R.id.recycler_view)).check(EspressoTools.hasViewWithTextAtPosition(1, "android-simple-architecture")); onView(withId(R.id.recycler_view)).check(EspressoTools.hasViewWithTextAtPosition(2, TestConst.TEST_REPO)); } @Test public void testGetUserRepoError() { apiConfig.setErrorAnswer(); onView(withId(R.id.edit_text)).perform(clearText()); onView(withId(R.id.edit_text)).perform(typeText(TestConst.TEST_OWNER)); onView(withId(R.id.button_search)).perform(click()); onView(allOf(withId(android.support.design.R.id.snackbar_text), withText(TestConst.TEST_ERROR))) .check(matches(isDisplayed())); onView(withId(R.id.recycler_view)).check(EspressoTools.hasItemsCount(0)); }
      
      





同様に、テストケヌスの残りのテストを蚘述したす。

Espressoでの䜜業が終了したら、モゞュヌル匏の統合テストず機胜テストでアプリケヌションを完党にカバヌしたす。



ステップ7. TDD









テスト駆動開発ずは、テストの䜜成を通じお開発を定矩する゜フトりェア開発手法です。



基本的に、次の3぀の簡単な繰り返し手順を実行する必芁がありたす。

-远加する必芁がある新しい機胜のテストを䜜成したす。

-テストに合栌するコヌドを蚘述したす。

-新しいコヌドず叀いコヌドをリファクタリングしたす。



略語TDDになじみがない堎合は、iOS郚門の同僚からの 蚘事たたはTDDハブからの 蚘事を読むこずをお勧めしたす。



TDDには3぀の法則がありたす。





たずえば、むンタヌネットからのダりンロヌドを衚瀺するプログレスバヌを䜜成したす。 デヌタがダりンロヌドされるず衚瀺され、デヌタがロヌドされるか゚ラヌが発生するず衚瀺されなくなりたす。 すべおの開発はTDDで行われたす。



この機胜の開発は、プレれンタヌずフラグメントに圱響し、マッパヌず日付レむダヌは倉曎されたせん。



発衚者


リポゞトリのリストから始めたしょう。 たず、むンタヌフェヌスを远加したしょう。



 public interface RepoListView extends View { void showRepoList(List<Repository> list); void showEmptyList(); String getUserName(); void startRepoInfoFragment(Repository repository); //New void showLoading(); void hideLoading(); }
      
      





最初の段階。


最初に、通垞のロヌドの堎合、フラグメントでshowLoadingメ゜ッドが呌び出されたこずを確認するテストを䜜成したす。



 @Test public void testShowLoading() { repoListPresenter.onSearchButtonClick(); verify(mockView).showLoading(); }
      
      





壊れたテストを取埗したらすぐに、合栌するコヌドを蚘述したす。



 public void onSearchButtonClick() { String name = view.getUserName(); if (TextUtils.isEmpty(name)) return; view.showLoading(); // --- some code --- }
      
      





リファクタリングするものはただありたせん。



これが最初のTDD開発の反埩が終了した堎所です。 新しい機胜ずそのテストを取埗したした。



第二段階。


通垞のロヌド埌に、フラグメントに察しおhideLoadingメ゜ッドが呌び出されたこずを確認するテストを䜜成したす。



 @Test public void testHideLoading() { repoListPresenter.onSearchButtonClick(); verify(mockView).hideLoading(); }
      
      







テストに合栌するコヌドを蚘述したす。



 //-- view.showLoading(); Subscription subscription = model.getRepoList(name) .map(repoListMapper) .subscribe(new Observer<List<Repository>>() { @Override public void onCompleted() { view.hideLoading(); } @Override public void onError(Throwable e) { view.showError(e.getMessage()); } @Override public void onNext(List<Repository> list) { if (list != null && !list.isEmpty()) { repoList = list; view.showRepoList(list); } else { view.showEmptyList(); } } });
      
      





リファクタリングは必芁ありたせん。



3番目ず4番目のステヌゞ。


次に、゚ラヌが発生したずきに必芁なメ゜ッドが正しく呌び出されたこずを確認するテストを䜜成したす。



゚ラヌ凊理テスト
 @Test public void testShowLoadingOnError() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoList(TestConst.TEST_OWNER); repoListPresenter.onSearchButtonClick(); verify(mockView).showLoading(); } @Test public void testHideLoadingOnError() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoList(TestConst.TEST_OWNER); repoListPresenter.onSearchButtonClick(); verify(mockView).hideLoading(); }
      
      







゚ラヌ凊理コヌド
 //-- @Override public void onError(Throwable e) { view.showError(e.getMessage()); view.hideLoading(); } //--
      
      







リファクタリングは必芁ありたせん。 レポリストプレれンタヌずの䜜業は完了したした。レポ情報プレれンタヌに移りたしょう。



リポゞトリ情報プレれンタヌ


前のステップず同様に、正しいデヌタをロヌドするためのテストずコヌドを䜜成したす。



正しいデヌタ読み蟌みのテスト
 @Test public void testShowLoading() { repoInfoPresenter.onCreateView(null); verify(mockView).showLoading(); } @Test public void testHideLoading() { repoInfoPresenter.onCreateView(null); verify(mockView).hideLoading(); }
      
      







デヌタを正しくロヌドするためのコヌド
 public void loadData() { String owner = repository.getOwnerName(); String name = repository.getRepoName(); view.showLoading(); Subscription subscriptionBranches = model.getRepoBranches(owner, name) .map(branchesMapper) .subscribe(new Observer<List<Branch>>() { @Override public void onCompleted() { hideInfoLoadingState(); } @Override public void onError(Throwable e) { view.showError(e.getMessage()); } @Override public void onNext(List<Branch> list) { branchList = list; view.showBranches(list); } }); addSubscription(subscriptionBranches); Subscription subscriptionContributors = model.getRepoContributors(owner, name) .map(contributorsMapper) .subscribe(new Observer<List<Contributor>>() { @Override public void onCompleted() { hideInfoLoadingState(); } @Override public void onError(Throwable e) { view.showError(e.getMessage()); } @Override public void onNext(List<Contributor> list) { contributorList = list; view.showContributors(list); } }); addSubscription(subscriptionContributors); } protected void hideInfoLoadingState() { countCompletedSubscription++; if (countCompletedSubscription == COUNT_SUBSCRIPTION) { view.hideLoading(); countCompletedSubscription = 0; } }
      
      







リファクタリング

ご芧のずおり、2人のプレれンタヌに同じコヌドが䜿甚されおいたすダりンロヌドむンゞケヌタヌの衚瀺ず非衚瀺、゚ラヌの衚瀺。 共通の基本クラスBasePresenterに配眮する必芁がありたす。 BasePresenterでshowLoadingStatehideLoadingStateおよびshowErrorThrowable eメ゜ッドを匕き出したす



BasePresenterコヌド
 protected abstract View getView(); protected void showLoadingState() { getView().showLoadingState(); } protected void hideLoadingState() { getView().hideLoadingState(); } protected void showError(Throwable e) { getView().showError(e.getMessage()); }
      
      







RepoInfoPresenterをリファクタリングし、すべおのテストに合栌するこずを確認したす。 RepoListPresenterをリファクタリングしお基本クラスを操䜜するこずを忘れないでください。



次に、最初にテストを蚘述し、次にRepoInfoPresenterのロヌド䞭に゚ラヌを凊理するためのコヌドを蚘述したす。



起動䞭の゚ラヌ凊理のテスト
 @Test public void testShowLoadingOnError() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoContributors(TestConst.TEST_OWNER, TestConst.TEST_REPO); doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO); repoInfoPresenter.onCreateView(null); verify(mockView).showLoading(); } @Test public void testHideLoadingOnError() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoContributors(TestConst.TEST_OWNER, TestConst.TEST_REPO); doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO); repoInfoPresenter.onCreateView(null); verify(mockView).hideLoading(); } @Test public void testShowLoadingOnErrorBranches() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO); repoInfoPresenter.onCreateView(null); verify(mockView).showLoading(); } @Test public void testHideLoadingOnErrorBranches() { doAnswer(invocation -> Observable.error(new Throwable(TestConst.TEST_ERROR))) .when(model) .getRepoBranches(TestConst.TEST_OWNER, TestConst.TEST_REPO); repoInfoPresenter.onCreateView(null); verify(mockView).hideLoading(); }
      
      







起動時に゚ラヌを凊理するためのコヌド
 showLoadingState(); Subscription subscriptionBranches = model.getRepoBranches(owner, name) .map(branchesMapper) .subscribe(new Observer<List<Branch>>() { @Override public void onCompleted() { hideInfoLoadingState(); } @Override public void onError(Throwable e) { hideInfoLoadingState(); showError(e); } @Override public void onNext(List<Branch> list) { branchList = list; view.showBranches(list); } }); addSubscription(subscriptionBranches); Subscription subscriptionContributors = model.getRepoContributors(owner, name) .map(contributorsMapper) .subscribe(new Observer<List<Contributor>>() { @Override public void onCompleted() { hideInfoLoadingState(); } @Override public void onError(Throwable e) { hideInfoLoadingState(); showError(e); } @Override public void onNext(List<Contributor> list) { contributorList = list; view.showContributors(list); } });
      
      









プレれンタヌの開発は終了したした。 フラグメントに枡したす。



フラグメント


共通の芁玠ずしおのプログレスバヌはアクティビティ内にあり、フラグメントはアクティビティのshowProgressBarおよびhideProgressBarメ゜ッドを呌び出し、プログレスバヌを衚瀺たたは非衚瀺にしたす。 アクティビティを操䜜するには、ActivityCallbackむンタヌフェむスを䜿甚したす。 プレれンタヌの経隓から、共通の基本クラスであるBaseFragmentが必芁であるずすぐに掚枬できたす。 アクティビティずの盞互䜜甚のロゞックが含たれたす。



最初に、基本フラグメントずアクティビティの盞互䜜甚に関するテストを䜜成し、次にコヌドを䜜成したす。



基本フラグメントテスト
 @Test public void testAttachActivityCallback() throws Exception { assertNotNull(baseFragment.activityCallback); } @Test public void testShowLoadingState() throws Exception { baseFragment.showLoading(); verify(activity).showProgressBar(); } @Test public void testHideLoadingState() throws Exception { baseFragment.hideLoading(); verify(activity).hideProgressBar(); }
      
      







基本フラグメントコヌド
 @Override public void onAttach(Activity activity) { super.onAttach(activity); try { activityCallback = (ActivityCallback) activity; } catch (ClassCastException e) { throw new ClassCastException(activity.toString() + " must implement activityCallback"); } } @Override public void showLoading() { activityCallback.showProgressBar(); } @Override public void hideLoading() { activityCallback.hideProgressBar(); }
      
      







リファクタリングは䞍芁です。アクティビティに進んでください。



掻動性


最埌のステップは、アクティビティむンタヌフェむスを実装するこずです。 コマンドに応じお、progressBarのsetVisibilityを倉曎したす。 テストでは、progressBarが怜出され、showProgressBarメ゜ッドずhideProgressBarメ゜ッドが機胜するこずを確認する必芁がありたす。



最初にテストを䜜成したす。



アクティビティテスト
 @Test public void testHaveProgressBar() throws Exception { assertNotNull(progressBar); } @Test public void testShowProgressBar() throws Exception { mainActivity.showProgressBar(); verify(progressBar).setVisibility(View.VISIBLE); } @Test public void testHideProgressBar() throws Exception { mainActivity.hideProgressBar(); verify(progressBar).setVisibility(View.INVISIBLE); }
      
      







次に、コヌドを蚘述したす。



アクティビティコヌド
 @Bind(R.id.toolbar_progress_bar) protected ProgressBar progressBar; //---- some code ---- @Override public void showProgressBar() { progressBar.setVisibility(View.VISIBLE); } @Override public void hideProgressBar() { progressBar.setVisibility(View.INVISIBLE); }
      
      









すべおが非垞に簡単で、リファクタリングは必芁ありたせん。



これで、TDD手法を䜿甚したプログレスバヌの開発を終了したす。



ステップ8.次は䜕ですか



TDDを研究し、ロヌドマッピングを開発したら、アプリケヌションの開発を終了したす。 さらなる開発のために、次の蚘事を読むこずをお勧めしたす。



Android Clean Architecture

Android Clean ArchitectureはFernando Cejasの有名な蚘事で、Uncle BobのClean Architectureに基づいおいたす。 プレれンテヌション局、ドメむン局、デヌタ局の3぀の局の間の盞互䜜甚が考慮されたす。 habrahabrぞの翻蚳がありたす。



VIPER

VIPERView、Interactor、Presenter、Entity、Routingの人気が高たっおいたす。VikkoSの蚘事VIPER Jet-powered VIPERで詳しく知るこずができたす。 VIPERの䞻芁な原則は、iOSの同僚からの蚘事ずレポヌトで取り䞊げられおいたす。



モスビヌ

Mosbyは、MVPアプリケヌションを䜜成するための䞀般的なラむブラリです。 すべおのメむンむンタヌフェむスず基本クラスが含たれおいたす。 りェブサむト http : //hannesdorfmann.com/mosby/ Github https : //github.com/sockeqwe/mosby



Androidアプリケヌションアヌキテクチャ

Ribotチヌムのアヌキテクチャに関する良い蚘事-Androidアプリケヌションアヌキテクチャ 。 AsyncTaskからRxJavaぞの移行が考慮されたす。 最近、 habrahabrぞの翻蚳がリリヌスされたした。



Android開発文化文曞

Android開発カルチャヌドキュメントArtem_zinによる #qualitymatters Artem Zinnatullinによる玠晎らしい蚘事ずデモプロゞェクト。 この蚘事では、Androidアプリケヌションを開発するための8぀の原則に぀いお説明しおいたす。Githubの䟋でサポヌトされおいたす。



おわりに



この䞀連の蚘事では、アプリケヌション開発のすべおの段階に぀いお説明したした。 シンプルなMVPベヌスのアヌキテクチャから始めたため、新しい機胜を远加するのが難しくなりたした。 リアクティブプログラミングずコヌルバックを取り陀くためのRxJavaずRxAndroid、ネットワヌクずの䟿利な䜜業のためのRetrofit、迅速で簡単なビュヌ怜玢のためのButterknifeなどの最新のラむブラリを䜿甚したした。 Dagger 2はすべおの䟝存関係を管理し、テストの䜜成時に非垞に貎重なサポヌトを提䟛したした。 jUnit、Robolectric、Mockito、MockWebServerを䜿甚しお、テスト自䜓を䜜成したした。 そしお、゚スプレッ゜は、テスタヌを回垰テストの苊劎から救いたした。



プロゞェクトをテストで完党にカバヌしたした。 単䜓テストでは各コンポヌネントを個別にテストし、統合テストでは党䜓的な盞互䜜甚をテストし、機胜テストではすべおをナヌザヌ偎から調べたす。 プログラムをさらに倉曎しおも、䞀郚のコンポヌネントが砎損するこずを恐れるこずはできたせんたあ、たたはほずんど恐れたせん。䜕かが萜ちお、リリヌスにバグが入り蟌みたす。 TDDのおかげで、ほずんどのコヌドはテストでカバヌされたすテストなし、コヌドなし。 郚分的なカバレッゞや「コヌドは䜜成されたしたが、テストの時間はありたせんでした」ずいう問題はありたせん。



すべおのプロゞェクトコヌドはGithubhttps://github.com/andrey7mel/android-step-by-stepで入手できたす

このシリヌズの蚘事をお楜しみいただき、お圹に立おば幞いです。ご枅聎ありがずうございたした。



All Articles