πŸ“œ ⬆️ ⬇️

Unit tests when using Corutin in Android application

image


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.


Typical architecture


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" } } 

Unit tests


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.


Using the CoroutineContextProvider Class


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