Unit tests when using corutin in an Android application

    image


    Translation of the article. The original is here .


    This article does not address the working principle of corutin. If you are not familiar with them, we recommend that you read the introduction to kotlinx git repo .


    The article describes the difficulties in writing unit tests for code that uses coroutines. In the end, we show a solution to this problem.


    Typical architecture


    Imagine we have a simple architecture MVPin an application. Activitylooks like that:


    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 Presenterwe use coroutines for asynchronous operations. The repository simply emulates the execution of a long request:


    // 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 the dependencies with explicit constructor use, testing our code will not be easy. We use the Mockito library for testing.
    It is also worth paying attention to the use of the function runBlocking. This is necessary to wait for the result of the test and use the supsendfunctions. 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 fails with:


    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 keyword opento the class ContentRepositoryand to the method requestContent()so that the library Mockitocan perform a function call override and an object override.


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

    The test fails again. This time, it happened because the context of the coroutine UIuses elements from the library Android.. Since we run tests for JVM, this leads to an error.


    We found a ready-made solution to this problem. You can see it here . The author solves this problem by moving Corutin's execution logic into Activity. It seems to us that this option is not too correct, because Activityassumes responsibility for managing workflows.


    Using the CoroutineContextProvider Class


    Here is another solution: pass the coroutine execution context using the constructor Presenter, and then use this context to launch coroutine. We need to create a 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 reference the same context as in the previous code. The class itself and its fields must have a modifier openin order to be able to inherit this class and redefine 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 UIand 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 TestContextProviderand add its use to the test.
    Grade TestContextProvider:


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

    We use context Unconfied. This means that coroutines are executed in the same thread as the rest of the code. He looks like a planner Trampolinein RxJava.


    Our last step is to pass TestContextProviderto the constructor Presenterin 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.


    Jabber is worth nothing - show us the code! Please - Link to git


    Also popular now: