Androidアプリケーションでコルチンを使用する場合の単体テスト

画像


記事の翻訳。 オリジナルはこちらです。


この記事では、コルチンの作用原理については触れていません。 それらに慣れていない場合は、 kotlinx git repoの概要を読むことをお勧めします。


この記事では、コルーチンを使用するコードの単体テストを書く際の困難について説明しています。 最後に、この問題の解決策を示します。


典型的なアーキテクチャ


アプリケーションに単純なMVPアーキテクチャがあると想像してください。 Activityは次のようになります。


 class ContentActivity : AppCompatActivity(), ContentView { private lateinit var textView: TextView private lateinit var presenter: ContentPresenter override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) setContentView(R.layout.activity_main) textView = findViewById(R.id.content_text_view) // emulation of dagger injectDependencies() presenter.onViewInit() } private fun injectDependencies() { presenter = ContentPresenter(ContentRepository(), this) } override fun displayContent(content: String) { textView.text = content } } // interface for View Presenter communication interface ContentView { fun displayContent(content: String) } 

Presenter非同期操作にコルーチンを使用します。 リポジトリは単純に長いリクエストの実行をエミュレートします:


 // Presenter class class ContentPresenter( private val repository: ContentRepository, private val view: ContentView ) { fun onViewInit() { launch(UI) { // move to another Thread val content = withContext(CommonPool) { repository.requestContent() } view.displayContent(content) } } } // Repository class class ContentRepository { suspend fun requestContent(): String { delay(1000L) return "Content" } } 

単体テスト


すべてうまくいきますが、今このコードをテストする必要があります。 明示的なコンストラクターの使用ですべての依存関係を紹介しますが、コードのテストは簡単ではないため、テストにはMockitoライブラリを使用します。
runBlocking関数の使用にも注意を払う必要があります。 これは、テストの結果を待機し、 supsend関数を使用するためにsupsendです。 テストコードは次のようになります。


 class ContentPresenterTest { @Test fun `Display content after receiving`() = runBlocking { // arrange val repository = mock(ContentRepository::class.java) val view = mock(ContentView::class.java) val presenter = ContentPresenter(repository, view) val expectedResult = "Result" `when`(repository.requestContent()).thenReturn(expectedResult) // act presenter.onViewInit() // assert verify(view).displayContent(expectedResult) } } 

テストは次のように失敗します。


org.mockito.exceptions.base.MockitoException: Cannot mock/spy class sample.dev.coroutinesunittests.ContentRepository Mockito cannot mock/spy because : — final class


Mockitoライブラリーが関数呼び出しをオーバーライドし、オブジェクト自体をオーバーライドできるように、 ContentRepositoryクラスとrequestContent()メソッドにopenキーワードを追加する必要があります。


  open class ContentRepository { suspend open fun requestContent(): String { delay(1000L) return "Content" } } 

テストは再び失敗します。 今回は、 UIコルーチンコンテキストがAndroid.ライブラリの要素を使用しているため、これが発生しましたAndroid.JVMテストを実行するため、これはエラーにつながります。


この問題に対する既成のソリューションを見つけました。 こちらで見れます 。 著者は、corutinの実行ロジックをActivity移動することにより、この問題を解決しています。 このオプションはあまり正確ではないようです、なぜなら Activityは、タスクのフローを管理する責任を負います。


CoroutineContextProviderクラスを使用する


もう1つの解決策は、 Presenterコンストラクターを使用してコルーチン実行コンテキストを渡し、このコンテキストを使用してコルーチンを起動することです。 CoroutineContextProviderクラスを作成する必要があります


 open class CoroutineContextProvider() { open val Main: CoroutineContext by lazy { UI } open val IO: CoroutineContext by lazy { CommonPool } } 

前のコードと同じコンテキストを参照するフィールドは2つだけです。 このクラスを継承し、テスト目的でフィールド値を再定義できるようにするには、クラス自体とそのフィールドにopen修飾子が必要です。 また、値を初めて使用するときにのみ、値を割り当てるために遅延初期化を使用する必要があります。 (それ以外の場合、クラスは常にUI値を初期化し、テストはまだ失敗します)


 // Presenter class class ContentPresenter( private val repository: ContentRepository, private val view: ContentView, private val contextPool: CoroutineContextProvider = CoroutineContextProvider() ) { fun onViewInit() { launch(contextPool.Main) { // move to another Thread val content = withContext(contextPool.IO) { repository.requestContent() } view.displayContent(content) } } } 

最後のステップは、 TestContextProviderを作成し、その使用をテストに追加することです。
クラスTestContextProvider


 class TestContextProvider : CoroutineContextProvider() { override val Main: CoroutineContext = Unconfined override val IO: CoroutineContext = Unconfined } 

Unconfiedコンテキストを使用します。 これは、コルーチンが残りのコードと同じスレッドで実行されることを意味します。 RxJava Trampolineプランナーのように見えます。


最後のステップは、 TestContextProviderをテストのPresenterコンストラクターに渡すことです。


 class ContentPresenterTest { @Test fun `Display content after receiving`() = runBlocking { // arrange val repository = mock(ContentRepository::class.java) val view = mock(ContentView::class.java) val presenter = ContentPresenter(repository, view, TestContextProvider()) val expectedResult = "Result" `when`(repository.requestContent()).thenReturn(expectedResult) // act presenter.onViewInit() // assert verify(view).displayContent(expectedResult) } } 

それだけです 次回の実行後、テストは成功します。


Jabberには何の価値もありません-コードを見せてください! お願い-gitへのリンク



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


All Articles