LiveData单元测试报getMainLooper not mocked错误如何解决
在ViewModel中使用LiveData持有响应状态,实现代码如下:
@HiltViewModel class MainViewModel @Inject constructor(private val repository: Repository): ViewModel() { private val _charData = MutableLiveData<Response<List<Character>>>() val charData: LiveData<Response<List<Character>>> get() = _charData init { getCharacters() } fun getCharacters(){ viewModelScope.launch { _charData.value = repository.getCharacters() } } }
执行单元测试时持续报错:Method getMainLooper in android.os.Looper not mocked.,报错定位在_charData.value = repository.getCharacters()这一行。按照同类问题的解决方案添加@get:Rule val instantExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule()后错误仍然存在,测试代码如下:
@RunWith(JUnit4::class) class MainViewModelTest { @get:Rule val mainRule = MainCoroutineRule() lateinit var repository: Repository lateinit var viewModel: MainViewModel @get:Rule val instantExecutorRule: InstantTaskExecutorRule = InstantTaskExecutorRule() @Before fun setUp(){ MockitoAnnotations.openMocks(this) repository = MockRepository() viewModel = MainViewModel(repository) } @Test fun getCharactersSuccess() = runTest { val observer = mock<Observer<Response<List<Character>>>>() viewModel.getCharacters() viewModel.charData.observeForever(observer) assertTrue(viewModel.charData.value is Response.Success) } }
这个报错由几个常见的测试配置疏漏共同导致,按以下步骤调整即可解决:
固定Rule执行顺序
多个@get:Rule同时声明时,JUnit默认不保证执行顺序,一旦协程规则先于InstantTaskExecutorRule生效,LiveData的setValue操作就会提前触发主线程Looper检查,直接抛出你看到的错误。同时你的ViewModel在init块中就会触发请求,必须保证所有规则在ViewModel初始化前完全生效。用RuleChain显式指定规则优先级,保证InstantTaskExecutorRule最先执行:@get:Rule val ruleChain: TestRule = RuleChain.outerRule(InstantTaskExecutorRule()) .around(MainCoroutineRule())删掉原来单独声明的
mainRule和instantExecutorRule即可。注意核对
InstantTaskExecutorRule的导入路径,不要引用过时的android.arch.core包下的类,必须使用androidx.arch.core.executor.testing.InstantTaskExecutorRule,否则规则不会生效。调整测试逻辑的执行顺序
你当前是先调用viewModel.getCharacters()再注册观察者,很容易出现数据发射时执行器还没完成hook的情况。把观察者注册提前到触发请求之前,同时主动等待协程执行完成,测试结束后移除观察者避免内存泄漏:@Test fun getCharactersSuccess() = runTest { val observer = mock<Observer<Response<List<Character>>>>() // 先注册观察者 viewModel.charData.observeForever(observer) // 再触发数据请求 viewModel.getCharacters() // 等待所有协程任务执行完成 advanceUntilIdle() assertTrue(viewModel.charData.value is Response.Success) // 移除观察者 viewModel.charData.removeObserver(observer) }补全本地单测配置
如果你是在src/test目录下做本地JVM单元测试(不是设备上的instrumented测试),需要在模块级build.gradle的android配置块中添加以下配置,避免Android框架存根方法抛出异常:android { // ... 其他现有配置 testOptions { unitTests.returnDefaultValues = true } }同时确认已经引入了必要的测试依赖,版本号和项目中现有lifecycle、协程版本对齐即可:
testImplementation "androidx.arch.core:core-testing:2.2.0" testImplementation "org.jetbrains.kotlinx:kotlinx-coroutines-test:1.6.4"
最后检查你的MainCoroutineRule实现是否符合标准:需要在规则初始化的before回调中把Dispatchers.Main替换为测试用的StandardTestDispatcher,在after回调中还原原有Main dispatcher,否则协程在主线程调度时也会触发Looper相关错误。
内容的提问来源于stack exchange,提问作者Tobi

