Translation of the article. The original is here .
This article does not address the principle of the work of korutin. If you are not familiar with them, then we recommend reading the introduction to kotlinx git repo .
This article describes the difficulties in writing unit tests for code that uses corutines. At the end we will show a solution to this problem.
Imagine that we have a simple MVP
architecture in the application. Activity
looks like this:
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) }
In Presenter
we use cortices for asynchronous operations. The repository simply emulates a long query:
// 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" } }
Everything works well, but now we need to test this code. Although we introduce all dependencies with an explicit use of the constructor, it will not be easy to test our code. We use the Mockito library for testing.
Also pay attention to the use of the runBlocking
function. This is necessary to wait for the result of the test and use the supsend
function. The test code looks like this:
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) } }
The test is performed with an error:
org.mockito.exceptions.base.MockitoException: Cannot mock/spy class sample.dev.coroutinesunittests.ContentRepository Mockito cannot mock/spy because : β final class
We need to add the open
keyword to the ContentRepository
class and to the requestContent()
method so that the Mockito
library can perform the substitution of a function call and the substitution of the object itself.
open class ContentRepository { suspend open fun requestContent(): String { delay(1000L) return "Content" } }
The test is again performed with an error. This time it happened due to the fact that the context of Korutina UI
uses elements from the Android.
library Android.
. Since we run tests for the JVM
, this causes an error.
We have found a ready-made solution to this problem. You can see it by reference . The author solves this problem by moving the logic of execution of korutin to the Activity
. We think this option is not too correct, because Activity
takes responsibility for managing task flows.
Here's another solution: pass the execution context to the coruntine using the Presenter
designer, and then use that context to launch the coruntine. We need to create the CoroutineContextProvider
class CoroutineContextProvider
open class CoroutineContextProvider() { open val Main: CoroutineContext by lazy { UI } open val IO: CoroutineContext by lazy { CommonPool } }
It has only two fields that refer to the same context as in the previous code. The class itself and its fields must have the open
modifier in order to be able to inherit this class and redefine the field values ββfor testing purposes. We also need to use lazy initialization to assign a value only when we use the value for the first time. (Otherwise the class always initializes the value of the UI
and the tests still fail)
// 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) } } }
The final step is to create a TestContextProvider
and add its use to the test.TestContextProvider
class:
class TestContextProvider : CoroutineContextProvider() { override val Main: CoroutineContext = Unconfined override val IO: CoroutineContext = Unconfined }
We use the Unconfied
context. This means that the cortins are executed in the same thread as the rest of the code. It is similar to the Trampoline
scheduler in RxJava
.
Our last step is to pass TestContextProvider
to the Presenter
constructor in the test:
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) } }
That's all. After the next run, the test will succeed.
Chatter is worth nothing - show us the code! Please - Link to git
Source: https://habr.com/ru/post/349228/
All Articles