Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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?

like image 559
ngu Avatar asked Oct 11 '18 14:10

ngu


People also ask

What is the difference between setValue and postValue in LiveData?

Live data is lifecycle-aware. postValue() is not lifecycle aware. The value will be updated whenever the main thread runs. In the case of setValue() the said value is called twice, and the value is updated twice, and the observers are notified about the updated data twice.

How do you observe LiveData in unit testing?

First, the LiveData should be observed in order to work properly. You can find a utility class at the end of the section to observe your LiveData from test class. Secondly we should add InstantExecutorRule test rule. InstantExecutorRule is a JUnit rule and it comes with androidx.

What is live data in Kotlin?

LiveData is a wrapper that can be used with any data, including objects that implement Collections , such as List . A LiveData object is usually stored within a ViewModel object and is accessed via a getter method, as demonstrated in the following example: Kotlin Java.


1 Answers

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)
}
like image 142
Demigod Avatar answered Sep 28 '22 04:09

Demigod