記事の翻訳。 オリジナルはこちらです。
この記事では、コルチンの作用原理については触れていません。 それらに慣れていない場合は、 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へのリンク