Search code examples
androidkotlinandroid-architecture-componentsandroid-livedataandroid-viewmodel

LiveData unit testing error when using postValue in init block


I'm trying to write a unit test for a view model using live data.

LoginViewModel.kt

class LoginViewModel @Inject constructor(
    val context: Context
): ViewModel() {
    val username = MutableLiveData<String>()
    val password = MutableLiveData<String>()
    val isLoginButtonEnabled = MediatorLiveData<Boolean>().apply {
        fun combineLatest(): Boolean {
            return !(username.value.isNullOrEmpty() || password.value.isNullOrEmpty())
        }
        addSource(username) { this.value = combineLatest() }
        addSource(password) { this.value = combineLatest() }
    }

    init {
        username.postValue("test")
        password.postValue("test")
    }
}

LoginViewModelTest.kt

@RunWith(MockitoJUnitRunner::class)
class LoginViewModelTest {
    @Rule
    @JvmField
    val instantTaskExecutorRole = InstantTaskExecutorRule()

    private val context = mock(Context::class.java)
    private val loginViewModel = LoginViewModel(context)

    @Test
    fun loginButtonDisabledOnEmptyUsername() {
        val observer = mock<Observer<Boolean>>()
        loginViewModel.isLoginButtonEnabled.observeForever(observer)
        loginViewModel.username.postValue("")

        verify(observer).onChanged(false)
    }
}

My unit test throws the following exception at the line username.postValue("test"):

java.lang.RuntimeException: Method getMainLooper in android.os.Looper not mocked. See http://g.co/androidstudio/not-mocked for details.

The InstantTaskExecutorRule should provide an execution context when using live data, however it doesn't work when initializing live data in the init-block. When omitting the init-block it works as desired, but i need the possibility to initialize live data variables.

Is there any way to make the live data initialization work when unit testing view models?


Solution

  • I managed to unit test my ViewModel that was using LiveData using mentioned rula - InstantTaskExecutorRule. But in my case the rule val declaration was a bit different:

    @Suppress("unused")
    @get:Rule
    val instantTaskExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()
    

    Edit:

    @Before
    @Throws(Exception::class)
    fun prepare() {
        MockitoAnnotations.initMocks(this)
    }
    

    Edit2:

    For some weird reason I cannot reproduce this :) Also, I think that the problem could be because of the way you're initializing your ViewModel -

    private val loginViewModel = LoginViewModel(context)
    

    I assume that it initializes too early, thus it's init block gets called too early too. Maybe it's reasonable to create it in the @Before method ? Like:

    private lateinit var viewModel: LoginViewModel
    
    @Before
    @Throws(Exception::class)
    fun prepare() {
        loginViewModel = LoginViewModel(context)
    }