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へのリンク








All Articles