23

I am trying to unit test my ViewModel that posts some stuff back to the activity via LiveData but when I run the method that sets the livedata value I get the error

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked.

I have read several posts and articles that all you have to do is add a Rule for InstantTaskExecutorRule and it should just work but I did that and I still get the error.

This is my unit test

val observer: Observer<String> = mock(Observer::class.java) as Observer<String>

@get:Rule
var rule: TestRule = InstantTaskExecutorRule()

@Test
fun testSearchDataValidationFailureMissingSearchLocation() {
    val viewModel = MoveViewModel()

    val param1 = 0
    val param2 = 1
    val param3 = "1234"
    viewModel.dialogMessageLiveData.observeForever(observer)

    Assert.assertFalse(viewModel.validateSearchData(param1, param2, param3))

    verify(observer).onChanged("Data not valid")
}

This is the method I am trying to test

fun validateSearchData(param1: Int, param2: Int, param3: String): Boolean {
    var valid: Boolean = false

    if (param1 == 0 || param2 == 0 || param3.isBlank()) {
        dialogMessageLiveData.postValue("Data not valid")
    } else {
        valid = true
    }

    return valid
}

I am not sure what else to do to fix this, can anyone suggest another solution?

alexrnov
  • 2,346
  • 3
  • 18
  • 34
tyczj
  • 71,600
  • 54
  • 194
  • 296
  • I have something very similar here that works....only possible difference that could be issue might be sequence of creation of `observer` mock and `rule` (I have `observer = mock()` in my `setUp()` method) – John O'Reilly Apr 10 '19 at 16:13
  • @JohnO'Reilly so you mean you have a `@Before` setup method where you set your observer? I just tried that too and no change – tyczj Apr 10 '19 at 16:28
  • 1
    Possible duplicate of [setValue and postValue on MutableLiveData in UnitTest](https://stackoverflow.com/questions/45988310/setvalue-and-postvalue-on-mutablelivedata-in-unittest) – AdamHurwitz Sep 08 '19 at 16:37

2 Answers2

2

you can use this extention to get the value of a [LiveData] or waits for it to have one, with a timeout. Use this extension from host-side (JVM) tests. It's recommended to use it alongside InstantTaskExecutorRule or a similar mechanism to execute tasks synchronously.

@VisibleForTesting(otherwise = VisibleForTesting.NONE)
fun <T> LiveData<T>.getOrAwaitValue(
    time: Long = 2,
    timeUnit: TimeUnit = TimeUnit.SECONDS,
    afterObserve: () -> Unit = {}
): T {
    var data: T? = null
    val latch = CountDownLatch(1)
    val observer = object : Observer<T> {
        override fun onChanged(o: T?) {
            data = o
            latch.countDown()
            this@getOrAwaitValue.removeObserver(this)
        }
    }
    this.observeForever(observer)

    try {
        afterObserve.invoke()

        // Don't wait indefinitely if the LiveData is not set.
        if (!latch.await(time, timeUnit)) {
            throw TimeoutException("LiveData value was never set.")
        }

    } finally {
        this.removeObserver(observer)
    }

    @Suppress("UNCHECKED_CAST")
    return data as T
}

then in your test case you can get the value

viewModel.dialogMessageLiveData.getOrAwaitValue() 
0

If you are reading this in 2021,

LiveData uses libraries that utilize threading. This uses the looper to communicate to the running thread.

This has to be Mocked for your tests to run.

@ExperimentalCoroutinesApi
class StarterViewModelTest : TestCase() {

@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()

private var handler = Mockito.mock(Handler::class.java)

lateinit var viewModel: StarterViewModel

@Before
override fun setUp() {
    viewModel = StarterViewModel(FakeBaseRepositorySuccess())
}


fun testGetGeoFences() {
    runTest {
        val resource = 
 viewModel.getGeoFences().getOrAwaitValueTesting()
        Assert.assertEquals(true, resource is Response.Success)
    }
}
}
Granson
  • 128
  • 1
  • 4