You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在Jetpack Compose+Hilt的Espresso测试中正确注入ViewModel?

问题

我正在使用Jetpack Compose和Hilt框架,MainActivity代码如下:

@AndroidEntryPoint
class MainActivity : ComponentActivity() {

    private val viewModel: MyViewModel by viewModels()

    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)

        setContent {
            val navController = rememberNavController()
            MainContent(
                navController = navController,
                viewModel = viewModel
            )
        }
    }
}

其中MainContent是一个Composable组件:

@Composable
fun MainContent(
    navController: NavHostController,
    viewModel: MyViewModel
   
) {
...
}

MyViewModel的定义如下:

@HiltViewModel
class MyViewModel @Inject constructor(
    @Named(STATE_FEED) private val _state: MutableStateFlow<Boolean>,
) : ViewModel() {
    val state: Flow<MyViewState> = _state
...
}

这是一个带有非空构造方法的ViewModel,正常运行应用时一切正常。但在运行UI自动化测试时:

@HiltAndroidTest
class MainActivityKtTest {

    @get:Rule(order = 0)
    val hiltTestRule by lazy { HiltAndroidRule(this) }

    @get:Rule(order = 1)
    val composeTestRule by lazy { createComposeRule() }

    private lateinit var navController: TestNavHostController

    private lateinit var context: Context

    private lateinit var appMockWebServer: MockWebServer

    @Before
    fun setUp() {
        hiltTestRule.inject()
        appMockWebServer = MockWebServer()
        appMockWebServer.start(BuildConfig.PORT)
        context = InstrumentationRegistry.getInstrumentation().context
    }

    @After
    fun tearDown() {
        appMockWebServer.shutdown()
    }

    @Test
    fun given_xxx_WHEN_yyy_THEN_zzz() {
        appMockWebServer.enqueueSuccess()

        composeTestRule.setContent {
            val viewModel: MyViewModel = hiltViewModel()
            navController = setTestNavController(context)
            MainContent(
                navController = navController,
                viewModel = viewModel
            )
        }
…
    }

我遇到错误,Hilt无法返回ViewModel实例,将ViewModel改为空构造方法则正常运行。请问使用val viewModel: MyViewModel = hiltViewModel()是否是正确的注入方式?

根据hiltViewModel()的源码注释:

/**
 * Returns an existing
 * [HiltViewModel](https://dagger.dev/api/latest/dagger/hilt/android/lifecycle/HiltViewModel)
 * -annotated [ViewModel] or creates a new one scoped to the current navigation graph present on
 * the {@link NavController} back stack.
 *
 * If no navigation graph is currently present then the current scope will be used, usually, a
 * fragment or an activity.
 *
 * @sample androidx.hilt.navigation.compose.samples.NavComposable
 * @sample androidx.hilt.navigation.compose.samples.NestedNavComposable
 */
@Composable
inline fun <reified VM : ViewModel> hiltViewModel(
    viewModelStoreOwner: ViewModelStoreOwner = checkNotNull(LocalViewModelStoreOwner.current) {
        "No ViewModelStoreOwner was provided via LocalViewModelStoreOwner"
    },
    key: String? = null
): VM {
    val factory = createHiltViewModelFactory(viewModelStoreOwner)
    return viewModel(viewModelStoreOwner, key, factory = factory)
}

它会返回已有的或创建新的HiltViewModel,作用域为NavController返回栈的当前导航图;若无导航图则使用当前作用域(通常是Fragment或Activity)。

但测试中直接调用Composable组件,上述两种作用域都不存在。若使用createAndroidComposeRule<MainActivity>(),会创建Activity并触发setContent,导致测试中重复调用setContent。请问如何正确在测试中注入ViewModel?

解决方案

1. 理解hiltViewModel()失败的原因

hiltViewModel()本身是正确的注入方式,但它依赖LocalViewModelStoreOwner.current提供作用域。纯Compose测试(用createComposeRule())默认没有关联的ViewModelStoreOwner(比如Activity、Fragment或导航图),所以Hilt无法找到合适的作用域来创建带构造参数的ViewModel实例。

2. 三种可行的测试注入方案

方案一:直接在测试类中注入ViewModel实例

测试类已经标注@HiltAndroidTest,可以直接通过Hilt注入ViewModel,再传递给Composable组件,完全绕开Compose中的作用域问题:

@HiltAndroidTest
class MainActivityKtTest {

    @get:Rule(order = 0)
    val hiltTestRule by lazy { HiltAndroidRule(this) }

    @get:Rule(order = 1)
    val composeTestRule by lazy { createComposeRule() }

    private lateinit var navController: TestNavHostController
    private lateinit var appMockWebServer: MockWebServer

    // 直接用Hilt注入ViewModel实例
    @Inject
    lateinit var viewModel: MyViewModel

    @Before
    fun setUp() {
        hiltTestRule.inject()
        appMockWebServer = MockWebServer()
        appMockWebServer.start(BuildConfig.PORT)
    }

    @After
    fun tearDown() {
        appMockWebServer.shutdown()
    }

    @Test
    fun given_xxx_WHEN_yyy_THEN_zzz() {
        appMockWebServer.enqueueSuccess()

        composeTestRule.setContent {
            navController = setTestNavController(InstrumentationRegistry.getInstrumentation().context)
            // 直接传入已注入的ViewModel
            MainContent(
                navController = navController,
                viewModel = viewModel
            )
        }

        // 执行测试断言逻辑
        ...
    }
}

方案二:使用createAndroidComposeRule<MainActivity>()关联Activity

用createAndroidComposeRule<MainActivity>()启动Activity后,替换其setContent逻辑,此时Activity作为ViewModelStoreOwner,Hilt可以正常创建ViewModel:

@HiltAndroidTest
class MainActivityKtTest {

    @get:Rule(order = 0)
    val hiltTestRule by lazy { HiltAndroidRule(this) }

    // 关联MainActivity,获取Activity实例控制权
    @get:Rule(order = 1)
    val composeTestRule by lazy { createAndroidComposeRule<MainActivity>() }

    private lateinit var navController: TestNavHostController
    private lateinit var appMockWebServer: MockWebServer

    @Before
    fun setUp() {
        hiltTestRule.inject()
        appMockWebServer = MockWebServer()
        appMockWebServer.start(BuildConfig.PORT)
    }

    @After
    fun tearDown() {
        appMockWebServer.shutdown()
    }

    @Test
    fun given_xxx_WHEN_yyy_THEN_zzz() {
        appMockWebServer.enqueueSuccess()

        // 替换Activity的setContent,避免重复调用
        composeTestRule.activity.setContent {
            navController = setTestNavController(composeTestRule.activity)
            // 此时有Activity作为作用域,hiltViewModel()可正常工作
            val viewModel: MyViewModel = hiltViewModel()
            MainContent(
                navController = navController,
                viewModel = viewModel
            )
        }

        // 执行测试断言逻辑
        ...
    }
}

方案三:手动提供ViewModelStoreOwner上下文

如果不想启动完整Activity,可以手动创建一个测试用的ViewModelStoreOwner,并通过CompositionLocalProvider注入Compose上下文:

@HiltAndroidTest
class MainActivityKtTest {

    @get:Rule(order = 0)
    val hiltTestRule by lazy { HiltAndroidRule(this) }

    @get:Rule(order = 1)
    val composeTestRule by lazy { createComposeRule() }

    private lateinit var navController: TestNavHostController
    private lateinit var appMockWebServer: MockWebServer
    // 创建测试用的ViewModelStoreOwner
    private val testViewModelStoreOwner = object : ViewModelStoreOwner {
        private val store = ViewModelStore()
        override fun getViewModelStore(): ViewModelStore = store
    }

    @Before
    fun setUp() {
        hiltTestRule.inject()
        appMockWebServer = MockWebServer()
        appMockWebServer.start(BuildConfig.PORT)
    }

    @After
    fun tearDown() {
        appMockWebServer.shutdown()
        // 清理ViewModelStore,避免内存泄漏
        testViewModelStoreOwner.viewModelStore.clear()
    }

    @Test
    fun given_xxx_WHEN_yyy_THEN_zzz() {
        appMockWebServer.enqueueSuccess()

        composeTestRule.setContent {
            // 手动提供ViewModelStoreOwner上下文
            CompositionLocalProvider(LocalViewModelStoreOwner provides testViewModelStoreOwner) {
                navController = setTestNavController(InstrumentationRegistry.getInstrumentation().context)
                val viewModel: MyViewModel = hiltViewModel()
                MainContent(
                    navController = navController,
                    viewModel = viewModel
                )
            }
        }

        // 执行测试断言逻辑
        ...
    }
}

3. 总结

优先选择方案一(直接注入ViewModel实例),这种方式最简洁,完全符合Hilt的测试设计;如果需要模拟完整的Activity场景,再选择方案二;方案三适合特殊场景下的轻量测试,但需要注意手动清理资源。

内容的提问来源于stack exchange,提问作者kingston

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.23 15:47:36