Search code examples
androidunit-testingkotlinkotlinx.coroutines

Unit testing coroutines on UI thread


I'm using coroutines to do an asynchronous call on pull to refresh like so:

class DataFragment : Fragment(), SwipeRefreshLayout.OnRefreshListener {

    // other functions here

    override fun onRefresh() {
        loadDataAsync()
    }

    private fun loadDataAsync() = async(UI) {
        swipeRefreshLayout?.isRefreshing = true
        progressLayout?.showContent()

        val data = async(CommonPool) {
            service?.getData() // suspending function
        }.await()

        when {
            data == null -> showError()
            data.isEmpty() -> progressLayout?.showEmpty(null, parentActivity?.getString(R.string.no_data), null)
            else -> {
                dataAdapter?.updateData(data)
                dataAdapter?.notifyDataSetChanged()
                progressLayout?.showContent()
            }
        }

        swipeRefreshLayout?.isRefreshing = false
    }
}

Everything here works fine when I actually put it on a device. My error, empty, and data states are all handled well and the performance is good. However, I'm also trying to unit test it with Spek. My Spek test looks like this:

@RunWith(JUnitPlatform::class)
class DataFragmentTest : Spek({

    describe("The DataFragment") {

        var uut: DataFragment? = null

        beforeEachTest {
            uut = DataFragment()
        }

        // test other functions

        describe("when onRefresh") {
            beforeEachTest {
                uut?.swipeRefreshLayout = mock()
                uut?.onRefresh()
            }

            it("sets swipeRefreshLayout.isRefreshing to true") {
                verify(uut?.swipeRefreshLayout)?.isRefreshing = true // says no interaction with mock
            }
        }
    }           
}

The test is failing because it says that there was no interaction with the uut?.swipeRefreshLayout mock. After some experimenting, it seems this is because I'm using the UI context via async(UI). If I make it just be a regular async, I can get the test to pass but then the app crashes because I'm modifying views outside of the UI thread.

Any ideas why this might be occurring? Also, if anyone has any better suggestions for doing this which will make it more testable, I'm all ears.

Thanks.

EDIT: Forgot to mention that I also tried wrapping the verify and the uut?.onRefresh() in a runBlocking, but I still had no success.


Solution

  • If you want to make things clean and consider using MVP architecture in the future you should understand that CourutineContext is external dependency, that should be injected via DI, or passed to your presenter. More details on topic.

    The answer for your question is simple, you should use only Unconfined CourutineContext for your tests. (more) To make things simple create an object e.g. Injection with:

    package com.example
    
    object Injection {
        val uiContext : CourutineContext = UI
        val bgContext : CourutineContext = CommonPool
    }
    

    and in test package create absolutely the same object but change to:

    package com.example
    
    object Injection {
        val uiContext : CourutineContext = Unconfined
        val bgContext : CourutineContext = Unconfined
    }
    

    and inside your class it will be something like:

    val data = async(Injection.bgContext) {service?.getData()}.await()