7

I've begun writing unit tests for my MVP Android project, but my tests dependent on coroutines intermittently fail (through logging and debugging I've confirmed verify sometimes occurs early, adding delay fixes this of course)

I've tried wrapping with runBlocking and I've discovered Dispatchers.setMain(mainThreadSurrogate) from org.jetbrains.kotlinx:kotlinx-coroutines-test, but trying so many combinations hasn't yielded any success so far.

abstract class CoroutinePresenter : Presenter, CoroutineScope {
    private lateinit var job: Job

    override val coroutineContext: CoroutineContext
        get() = job + Dispatchers.Main

    override fun onCreate() {
        super.onCreate()
        job = Job()
    }

    override fun onDestroy() {
        super.onDestroy()
        job.cancel()
    }
}

class MainPresenter @Inject constructor(private val getInfoUsecase: GetInfoUsecase) : CoroutinePresenter() {
    lateinit var view: View

    fun inject(view: View) {
        this.view = view
    }

    override fun onResume() {
        super.onResume()

        refreshInfo()
    }

    fun refreshInfo() = launch {
        view.showLoading()
        view.showInfo(getInfoUsecase.getInfo())
        view.hideLoading()
    }

    interface View {
        fun showLoading()
        fun hideLoading()

        fun showInfo(info: Info)
    }
}

class MainPresenterTest {
    private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread")

    private lateinit var presenter: MainPresenter
    private lateinit var view: MainPresenter.View

    val expectedInfo = Info()

    @Before
    fun setUp() {
        Dispatchers.setMain(mainThreadSurrogate)

        view = mock()

        val mockInfoUseCase = mock<GetInfoUsecase> {
            on { runBlocking { getInfo() } } doReturn expectedInfo
        }

        presenter = MainPresenter(mockInfoUseCase)
        presenter.inject(view)
        presenter.onCreate()
    }

    @Test
    fun onResume_RefreshView() {
        presenter.onResume()

        verify(view).showLoading()
        verify(view).showInfo(expectedInfo)
        verify(view).hideLoading()
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
        mainThreadSurrogate.close()
    }
}

I believe the runBlocking blocks should be forcing all child coroutineScopes to run on the same thread, forcing them to complete before moving on to verification.

2
  • Could you please provide code of your Presenter and complete code of test class. Commented Dec 23, 2018 at 7:28
  • @Sergey I've expanded my code samples as requested, I hope it helps Commented Dec 23, 2018 at 13:18

2 Answers 2

5

In CoroutinePresenter class you are using Dispatchers.Main. You should be able to change it in the tests. Try to do the following:

  1. Add uiContext: CoroutineContext parameter to your presenters' constructor:

    abstract class CoroutinePresenter(private val uiContext: CoroutineContext = Dispatchers.Main) : CoroutineScope {
    private lateinit var job: Job
    
    override val coroutineContext: CoroutineContext
        get() = uiContext + job
    
    //...
    }
    
    class MainPresenter(private val getInfoUsecase: GetInfoUsecase, 
                        private val uiContext: CoroutineContext = Dispatchers.Main 
    ) : CoroutinePresenter(uiContext) { ... }
    
  2. Change MainPresenterTest class to inject another CoroutineContext:

    class MainPresenterTest {
        private lateinit var presenter: MainPresenter
    
        @Mock
        private lateinit var view: MainPresenter.View
    
        @Mock
        private lateinit var mockInfoUseCase: GetInfoUsecase
    
        val expectedInfo = Info()
    
        @Before
        fun setUp() {
            // Mockito has a very convenient way to inject mocks by using the @Mock annotation. To
            // inject the mocks in the test the initMocks method needs to be called.
            MockitoAnnotations.initMocks(this)
    
            presenter = MainPresenter(mockInfoUseCase, Dispatchers.Unconfined) // here another CoroutineContext is injected 
            presenter.inject(view)
            presenter.onCreate()
    }
    
        @Test
        fun onResume_RefreshView() = runBlocking {
            Mockito.`when`(mockInfoUseCase.getInfo()).thenReturn(expectedInfo)
    
            presenter.onResume()
    
            verify(view).showLoading()
            verify(view).showInfo(expectedInfo)
            verify(view).hideLoading()
        }
    }
    
Sign up to request clarification or add additional context in comments.

1 Comment

Re-architecting so the CoroutineScope was able to be mocked with Dispatchers.Unconfined worked as expected. Thanks for your help!
2

@Sergey's answer caused me to read further into Dispatchers.Unconfined and I realised that I was not using Dispatchers.setMain() to its fullest extent. At the time of writing, note this solution is experimental.

By removing any mention of:

private val mainThreadSurrogate = newSingleThreadContext("Mocked UI thread") 

and instead setting the main dispatcher to

Dispatchers.setMain(Dispatchers.Unconfined)

This has the same result.

A less idiomatic method but one that may assist anyone as a stop-gap solution is to block until all child coroutine jobs have completed (credit: https://stackoverflow.com/a/53335224/4101825):

this.coroutineContext[Job]!!.children.forEach { it.join() }

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.