2種類の単体テストでアプリケーションをより安定させる方法

こんにちは、Habr。 私の名前はイリヤ・スミルノフです。私はフィンチのAndroid開発者です。 私たちのチームで開発した単体テストの使用例をいくつか紹介します。



プロジェクトでは、コンプライアンスチェックとコールチェックの2種類の単体テストが使用されます。 それぞれについて詳しく見ていきましょう。



コンプライアンステスト



コンプライアンステストは、一部の機能の実行の実際の結果が期待される結果と一致するかどうかを検証します。 例を示しましょう-その日のニュースのリストを表示するアプリケーションがあると想像してください:







ニュースデータはさまざまなソースから取得され、ビジネスレイヤーの出口で次のモデルに変わります。



data class News( val text: String, val date: Long )
      
      





アプリケーションロジックによると、リストの各要素には次の形式のモデルが必要です。



 data class NewsViewData( val id: String, val title: String, val description: String, val date: String )
      
      





次のクラスは、 ドメインモデルをビューモデルに変換する役割を果たします



 class NewsMapper { fun mapToNewsViewData(news: List<News>): List<NewsViewData> { return mutableListOf<NewsViewData>().apply{ news.forEach { val textSplits = it.text.split("\\.".toRegex()) val dateFormat = SimpleDateFormat("yyyy-MM-dd HH:mm", Locale("ru")) add( NewsViewData( id = it.date.toString(), title = textSplits[0], description = textSplits[1].trim(), date = dateFormat.format(it.date) ) ) } } } }
      
      





だから、オブジェクト



 News( "Super News. Some description and bla bla bla", 1551637424401 )
      
      





何らかのオブジェクトに変換されます



 NewsViewData( "1551637424401", "Super News", "Some description and bla bla bla", "2019-03-03 21:23" )
      
      





入力データと出力データは既知であるため、 mapToNewsViewDataメソッドのテストを記述できます。このメソッドは、入力に応じて出力データのコンプライアンスをチェックします。



これを行うには、app / src / test / ...フォルダーで、次の内容のNewsMapperTestクラス作成します。



 class NewsMapperTest { private val mapper = NewsMapper() @Test fun mapToNewsViewData() { val inputData = listOf( News("Super News. Some description and bla bla bla", 1551637424401) ) val outputData = mapper.mapToNewsViewData(inputData) Assert.assertEquals(outputData.size, inputData.size) outputData.forEach { Assert.assertEquals(it.id, "1551637424401") Assert.assertEquals(it.title, "Super News") Assert.assertEquals(it.description, "Some description and bla bla bla") Assert.assertEquals(it.date, "2019-03-03 21:23") } } }
      
      





得られた結果は、 org.junit.Assertパッケージのメソッドを使用して期待値と比較されます。 いずれかの値が期待を満たしていない場合、テストは失敗します。



テストされたクラスのコンストラクターがいくつかの依存関係を取る場合があります。 リソースにアクセスするための単純なResourceManagerか、ビジネスロジックを実行するための完全なInteractorのいずれかです。 このような依存関係のインスタンスを作成できますが、同様のモックオブジェクトを作成することをお勧めします。 モックオブジェクトは、内部メソッドの呼び出しを追跡し、戻り値をオーバーライドできるクラスの架空の実装を提供します。



モックを作成するためのポピュラーなMockitoフレームワークがあります。

Kotlinでは、デフォルトですべてのクラスがfinalであるため、Mockitoでモックオブジェクトを最初から作成することはできません。 この制限を回避するには、 mockito-inline依存関係を追加することをお勧めします。



テストの作成時にkotlin dslを使用する場合、 Mockito-Kotlinなどのさまざまなライブラリを使用できます。



NewsMapperが特定のNewsRepoを依存関係の形式で取り、特定のニュースアイテムを表示しているユーザーに関する情報を記録するとします 。 次に、 NewsRepoのモックを作成し、 isNewsReadの結果に応じてmapToNewsViewDataメソッドの戻り値を確認するのが理にかなっています。



 class NewsMapperTest { private val newsRepo: NewsRepo = mock() private val mapper = NewsMapper(newsRepo) … @Test fun mapToNewsViewData_Read() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(true) ... } @Test fun mapToNewsViewData_UnRead() { whenever(newsRepo.isNewsRead(anyLong())).doReturn(false) ... } … }
      
      





したがって、モックオブジェクトを使用すると、戻り値のさまざまなオプションをシミュレートして、さまざまなテストケースをテストできます。



上記の例に加えて、適合性テストにはさまざまなデータ検証ツールが含まれています。 たとえば、入力されたパスワードで特殊文字の有無と最小長を確認する方法。



通話テスト



呼び出しのテストでは、あるクラスのメソッドが別のクラスの必要なメソッドを呼び出すかどうかを確認します。 ほとんどの場合、このようなテストはPresenterに適用されます。Presenterは 、状態を変更するためのView固有のコマンドを送信します。 ニュースリストの例に戻る:



 class MainPresenter( private val view: MainView, private val interactor: NewsInteractor, private val mapper: NewsMapper ) { var scope = CoroutineScope(Dispatchers.Main) fun onCreated() { view.setLoading(true) scope.launch { val news = interactor.getNews() val newsData = mapper.mapToNewsViewData(news) view.setLoading(false) view.setNewsItems(newsData) } } … }
      
      





ここで最も重要なことは、 InteractorおよびViewからメソッドを呼び出すという事実です。 テストは次のようになります。



 class MainPresenterTest { private val view: MainView = mock() private val mapper: NewsMapper = mock() private val interactor: NewsInteractor = mock() private val presenter = MainPresenter(view, interactor, mapper).apply { scope = CoroutineScope(Dispatchers.Unconfined) } @Test fun onCreated() = runBlocking { whenever(interactor.getNews()).doReturn(emptyList()) whenever(mapper.mapToNewsViewData(emptyList())).doReturn(emptyList()) presenter.onCreated() verify(view, times(1)).setLoading(true) verify(interactor).getNews() verify(mapper).mapToNewsViewData(emptyList()) verify(view).setLoading(false) verify(view).setNewsItems(emptyList()) } }
      
      





プラットフォームの依存関係をテストから除外するには、さまざまなソリューションが必要になる場合があります。 それはすべて、マルチスレッドを処理するためのテクノロジーに依存しています。 上記の例では、オーバーライドされたスコープを持つKotlinコルーチンを使用して、テストを実行します。 Dispatchers.Mainプログラムコードで使用されているのは、このタイプのテストでは受け入れられないAndroid UIスレッドを指します。 RxJavaを使用するには、コード実行フローを切り替えるTestRuleを作成するなど、他のソリューションが必要です。



メソッドが呼び出されたことを確認するには、verifyメソッドを使用します。このメソッドは、テストされるメソッドの呼び出し回数を示すメソッドを追加の引数として取得できます。



*****



考慮されるテストオプションは、コードのかなりの部分をカバーできるため、アプリケーションの安定性と予測性が向上します。 テストでカバーされたコードは、保守が容易であり、スケーリングが容易です。 新しい機能を追加しても何も壊れないというある程度の自信があります。 そしてもちろん、そのようなコードはリファクタリングが簡単です。



テストが最も簡単なクラスには、プラットフォームの依存関係が含まれていません。 それを使用する場合、プラットフォームのモックオブジェクトを作成するためのサードパーティのソリューションは必要ありません。 したがって、プロジェクトでは、テスト対象のレイヤーでプラットフォームの依存関係の使用を最小限に抑えるアーキテクチャを使用しています。



良いコードはテスト可能でなければなりません。 通常、単体テストを作成する複雑さや不可能性は、テスト対象のコードに何か問題があることを示しており、リファクタリングについて考える時が来ました。



この例のソースコードはGitHubで入手できます



All Articles