Android测试:Dagger注入ViewModel的Fragment集成测试报空指针异常
问题:Fragment集成测试中ViewModelProvider的owner参数空指针异常
现有实现
Fragment中获取ViewModel的代码
Fragment通过lazy委托从导航图的ViewModelStore中获取实例:
private val myViewModel: MyViewModel by lazy { ViewModelProvider(findNavController().getBackStackEntry(R.id.nav_graph), Factory(this) { stateHandle -> (activity as MainActivity).myComponent.myViewModel() .create(stateHandle)}).get(MyViewModel::class.java) }
ViewModel的实现
ViewModel使用@AssistedInject和@AssistedFactory处理SavedStateHandle:
class MyViewModel @AssistedInject constructor( private val myRepository: MyRepository, @Assisted val savedStateHandle: SavedStateHandle ) : ViewModel() { @AssistedFactory interface MyViewModelFactory : ViewModelProvider.Factory { fun create(savedStateHandle: SavedStateHandle) : MyViewModel } // 其他ViewModel代码 }
测试报错信息
应用运行正常,但集成测试时抛出空指针异常:
java.lang.NullPointerException: Parameter specified as non-null is null: method androidx.lifecycle.ViewModelProvider.<init>, parameter owner at androidx.lifecycle.ViewModelProvider.<init>(Unknown Source:2)
原因是findNavController().getBackStackEntry(R.id.nav_graph)返回null,导致ViewModelProvider的owner参数为空。
当前测试代码
@RunWith(AndroidJUnit4::class) class MyFragmentTest { @get:Rule var activityRule = ActivityScenarioRule(MainActivity::class.java) private lateinit var scenario: FragmentScenario<MyFragment> private lateinit var binding: FragmentMyBinding private lateinit var mockNavController : NavController @Test fun setUp() { mockNavController = mock(NavController::class.java) scenario = launchFragmentInContainer<MyFragment> { MyFragment().also { fragment -> fragment.viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner -> if (viewLifecycleOwner != null) { // The fragment’s view has just been created mockNavController.setGraph(R.navigation.nav_graph) Navigation.setViewNavController(fragment.requireView(), mockNavController) } } } } }
解决方案
1. 修正测试方法的生命周期注解
setUp是JUnit的初始化方法,不能用@Test标注,应该替换为@Before,并单独编写测试方法。
2. 模拟有效的BackStackEntry
当前mock的NavController没有返回有效的BackStackEntry,这是导致NPE的核心原因。需要mockSavedStateViewModelStoreOwner(BackStackEntry的父接口),并让NavController返回该实例。
3. 注入Mock ViewModel
测试中无需依赖真实的Dagger组件,直接提供mock的ViewModel实例,隔离底层依赖。
修改后的测试代码示例:
@RunWith(AndroidJUnit4::class) class MyFragmentTest { private lateinit var mockNavController: NavController private lateinit var mockBackStackEntry: SavedStateViewModelStoreOwner private lateinit var mockViewModel: MyViewModel @Before fun setUp() { // 初始化所有mock对象 mockNavController = mock(NavController::class.java) mockBackStackEntry = mock(SavedStateViewModelStoreOwner::class.java) mockViewModel = mock(MyViewModel::class.java) // 让NavController返回mock的BackStackEntry `when`(mockNavController.getBackStackEntry(R.id.nav_graph)).thenReturn(mockBackStackEntry) // 为mock的BackStackEntry提供有效的ViewModelStore val viewModelStore = ViewModelStore() `when`(mockBackStackEntry.viewModelStore).thenReturn(viewModelStore) } @Test fun testFragmentUIElements() { // 启动Fragment并设置导航控制器 launchFragmentInContainer<MyFragment> { viewLifecycleOwnerLiveData.observeForever { viewLifecycleOwner -> if (viewLifecycleOwner != null) { Navigation.setViewNavController(requireView(), mockNavController) // 将mock的ViewModel注入到导航栈的ViewModelStore中 ViewModelProvider(mockBackStackEntry, object : ViewModelProvider.Factory { override fun <T : ViewModel> create(modelClass: Class<T>): T { return mockViewModel as T } }).get(MyViewModel::class.java) } } } // 示例:验证UI控件是否正常显示 onView(withId(R.id.your_ui_element_id)).check(matches(isDisplayed())) } }
关键说明
- 生命周期修正:
@Before注解的方法会在每个测试用例执行前初始化mock对象,确保测试环境一致。 - BackStackEntry模拟:必须让
getBackStackEntry返回非null实例,否则ViewModelProvider无法初始化。这里通过mockSavedStateViewModelStoreOwner并提供ViewModelStore解决。 - Mock ViewModel注入:直接使用mock的ViewModel可以隔离Repository等依赖,让测试专注于UI逻辑验证,无需启动完整的Dagger组件。
内容的提问来源于stack exchange,提问作者MajinKenn
相关产品推荐
相关产品推荐

