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?
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)
}