Skip to content

Test helper to use Dispatchers.Main without framework dependencies on JVM #568

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
objcode opened this issue Sep 16, 2018 · 4 comments
Closed

Comments

@objcode
Copy link
Contributor

objcode commented Sep 16, 2018

Report version: coroutines 0.26.

In the current implementation of HandlerDispatcher it is not possible to run code that uses Dispatchers.Main in pure-Kotlin tests.

Minimal example:

// MyViewModel.kt
class MyViewModel: ViewModel() {
  val parentJob = Job()
  val uiScope = CoroutineScope(Dispatchers.Main + parentJob)
}
// MyViewModelTest.kt which runs as a unit test without the Android framework
class MyViewModelTest {
   @Test
   fun aTest() {
      val subject = MyViewModel()
   }
}

This leads to a not mocked error.

Caused by: java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.
	at android.os.Looper.getMainLooper(Looper.java)
	at kotlinx.coroutines.experimental.android.HandlerDispatcherKt.<clinit>(HandlerDispatcher.kt:43)

A typical testing solution on Android is to replace the main looper with an immediate executor in test contexts. For example see InstantTaskExecutorRule from Room.

Taking a look at HandlerDispatcher, it appears it's not possible to avoid the call to Looper.getMainLooper as it's used to assign mainHandler, and HandlerDispatcher is listed as a sealed class so the only option to get past this import error in the current implementation is to add indirection to references to Dispatcher.Main.

@zach-klippenstein
Copy link
Contributor

Just pass the dispatcher into your view model constructor (or using a DI framework), then you can use a different dispatcher in unit tests.

@elizarov
Copy link
Contributor

There is one potential enhancement we can discuss (I'm not 100% sure it is a good idea, though). Here it is. We could detect wether you are really running your code under Android, in which case Dispatchers.Main works properly as it is now, or if your code run with mocked Android classes, where using Dispatchers.Main currently fails. In the later case we can see if you run inside a runBlocking { .. } and if you do, then use its single-threaded dispatcher for Dispatchers.Main. That means, that if you write your tests like this:

   @Test
   fun aTest() = runBlocking<Unit> {
      val subject = MyViewModel()
   }

Then Dispatchers.Main would work by scheduling in the main thread that runs the tests.

What do you think?

@objcode
Copy link
Contributor Author

objcode commented Oct 16, 2018

Zach, agreed, dependency injection (or more likely a service locator pattern here) solves the problem for a specific code base.

@elizarov that is an interesting proposal - I see a few ways to consider this (for mockable jar only)

After working through them, I'm not sure this is quite the right API.

What is needed in the general case of tests with a testing handler is a way to control time. Infinite clocks that reschedule themselves can then be tested by stoping time for example. Building such a handler may be out of the scope of the coroutines library, but it'd be great to put in hooks to switch out the handler with custom implementations.

WDYT?

Test framework that doesn't support coroutines

Any test written against coroutine code that uses withContext(Dispatchers.Main) will need to be inside runBlocking.

However, a context may be eagerly created at subject creation time (as you demonstrated) so this would need an effective error message.

Test framework that supports coroutines

Such a framework will probably use a custom dispatcher for the reasons listed below.

Custom testing dispatcher

One might do this to execute time-delays faster, or to increase the verbosity of logs on failures. I'm not sure how it would detect it's in a runBlocking section.

What about withContext(Dispatchers.Main)

A common pattern I'm seeing is to launch everything in a background thread then use withContext(Dispatchers.Main) to switch back to the main thread on results. This pattern will be surprising if the runBlocking change to the Main dispatcher is passed down through several scopes.

@objcode
Copy link
Contributor Author

objcode commented Dec 7, 2018

This will be resolved with #749 closing this issue.

@objcode objcode closed this as completed Dec 7, 2018
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants