在 init 块中使用 postValue 时出现 LiveData 单元测试错误

LiveData unit testing error when using postValue in init block

我正在尝试使用实时数据为视图模型编写单元测试。

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

我的单元测试在 username.postValue("test") 行抛出以下异常:

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

InstantTaskExecutorRule 应该在使用实时数据时提供执行上下文,但是在 init 块中初始化实时数据时它不起作用。当省略 init-block 时,它会按预期工作,但我需要初始化实时数据变量的可能性。

在对视图模型进行单元测试时,有什么方法可以使实时数据初始化工作吗?

我设法使用提到的规则 - InstantTaskExecutorRule 对使用 LiveDataViewModel 进行了单元测试。但在我的例子中,规则 val 声明有点不同:

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

编辑:

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

编辑2:

出于某种奇怪的原因,我无法重现它:) 另外,我认为问题可能是因为您初始化 ViewModel 的方式 -

private val loginViewModel = LoginViewModel(context)

我假设它初始化得太早,因此它的 init 块也被调用得太早。也许在 @Before 方法中创建它是合理的?喜欢:

private lateinit var viewModel: LoginViewModel

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

我在 ViewModelinit 期间设置 LiveData 值时遇到了类似的问题。 Demigod 的解决方案为我指明了正确的方向,但我想解释一下在测试过程的生命周期中发生了什么以及为什么。

当您有一个 ViewModelinit 期间设置 LiveData 时,视图模型一初始化就会 运行。当您使用 val viewModel = MyViewModel() 在单元测试中初始化视图模型时,该视图模型会在初始化测试 class 的同时实例化。问题是你可能同时初始化了任何规则,但实际上 运行 直到 class 完全初始化之后,所以你的 ViewModel.init() 发生在规则实际生效之前。这意味着您的实时数据无法在即时执行器上运行,任何 Rx 可观察对象都不会 运行 在替换的调度程序上,等等。因此最终有两种解决方法:

  1. 将视图模型定义为 lateinit var 并在测试的 @Before 方法中将视图模型初始化为 a,在应用规则后 运行s,或者
  2. 将视图模型定义为 val viewModel by lazy { MyViewModel() },在您真正开始在测试中调用它之前,它不会是 运行。

我更喜欢选项 2,因为它还允许我在初始化视图模型之前设置任何特定于测试用例的先决条件,而且我不必执行重复的初始化代码(这可能非常冗长)在每个需要它的测试中。

我遇到了类似的问题,半神提供的答案没有解决。我终于发现了魔鬼藏身之处,所以我在这里分享它:我的 init 块是在 liveData 初始化之前设置的,它在 运行 应用程序时工作正常,但在 运行 测试时却不行!

class MyViewModel : ViewModel() {

//    init {       // <-- Do not put the init block before the liveData
//        _myLiveData.postValue("First")
//    }

    private val _myLiveData: MutableLiveData<String> = MutableLiveData()
    val myLiveData: LiveData<String>
        get() = _myLiveData

    init {
        _myLiveData.postValue("First")
    }
}