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

请求提供含Dagger Hilt的Jetpack Compose测试Demo(含ViewModel与Fake Repository)

Dagger Hilt + Jetpack Compose 测试示例(含Fake Repository)

一、项目依赖配置

首先在项目根目录的build.gradle和模块的build.gradle中添加必要依赖:

根build.gradle(Project级)

plugins {
    id 'com.android.application' version '8.1.0' apply false
    id 'com.android.library' version '8.1.0' apply false
    id 'org.jetbrains.kotlin.android' version '1.9.0' apply false
    id 'com.google.dagger.hilt.android' version '2.48' apply false
}

模块build.gradle(App级)

plugins {
    id 'com.android.application'
    id 'org.jetbrains.kotlin.android'
    id 'com.google.dagger.hilt.android'
    id 'kotlin-kapt'
}

android {
    namespace "com.example.hiltcomposeexample"
    compileSdk 34

    defaultConfig {
        applicationId "com.example.hiltcomposeexample"
        minSdk 24
        targetSdk 34
        versionCode 1
        versionName "1.0"

        testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
        vectorDrawables {
            useSupportLibrary true
        }
    }

    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro'
        }
    }
    compileOptions {
        sourceCompatibility JavaVersion.VERSION_1_8
        targetCompatibility JavaVersion.VERSION_1_8
    }
    kotlinOptions {
        jvmTarget = '1.8'
    }
    buildFeatures {
        compose true
    }
    composeOptions {
        kotlinCompilerExtensionVersion '1.5.3'
    }
    packaging {
        resources {
            excludes += '/META-INF/{AL2.0,LGPL2.1}'
        }
    }
}

dependencies {
    // Jetpack Compose
    implementation 'androidx.core:core-ktx:1.12.0'
    implementation 'androidx.lifecycle:lifecycle-runtime-ktx:2.6.2'
    implementation 'androidx.activity:activity-compose:1.8.1'
    implementation platform('androidx.compose:compose-bom:2023.10.01')
    implementation 'androidx.compose.ui:ui'
    implementation 'androidx.compose.ui:ui-graphics'
    implementation 'androidx.compose.ui:ui-tooling-preview'
    implementation 'androidx.compose.material3:material3'

    // Dagger Hilt
    implementation 'com.google.dagger:hilt-android:2.48'
    kapt 'com.google.dagger:hilt-android-compiler:2.48'
    implementation 'androidx.hilt:hilt-navigation-compose:1.1.0'

    // ViewModel
    implementation 'androidx.lifecycle:lifecycle-viewmodel-compose:2.6.2'

    // Testing
    testImplementation 'junit:junit:4.13.2'
    androidTestImplementation 'androidx.test.ext:junit:1.1.5'
    androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
    androidTestImplementation platform('androidx.compose:compose-bom:2023.10.01')
    androidTestImplementation 'androidx.compose.ui:ui-test-junit4'
    debugImplementation 'androidx.compose.ui:ui-tooling'
    debugImplementation 'androidx.compose.ui:ui-test-manifest'

    // Hilt Testing
    androidTestImplementation 'com.google.dagger:hilt-android-testing:2.48'
    kaptAndroidTest 'com.google.dagger:hilt-android-compiler:2.48'
    testImplementation 'com.google.dagger:hilt-android-testing:2.48'
    kaptTest 'com.google.dagger:hilt-android-compiler:2.48'
}

二、数据层:Repository接口与实现

1. Repository接口定义

创建UserRepository.kt,定义业务操作接口:

interface UserRepository {
    suspend fun getUsers(): Result<List<User>>
}

data class User(val id: Int, val name: String, val email: String)

2. 真实Repository实现

UserRepositoryImpl.kt,模拟真实数据源(比如网络请求):

class UserRepositoryImpl : UserRepository {
    override suspend fun getUsers(): Result<List<User>> {
        // 模拟网络请求延迟
        kotlinx.coroutines.delay(1000)
        // 返回成功数据,可模拟错误场景
        return Result.success(
            listOf(
                User(1, "Alice Smith", "alice@example.com"),
                User(2, "Bob Johnson", "bob@example.com")
            )
        )
    }
}

3. Fake Repository(测试用)

FakeUserRepository.kt,用于测试时替换真实实现:

class FakeUserRepository : UserRepository {
    // 可配置返回结果,方便测试不同状态
    var shouldReturnError = false

    override suspend fun getUsers(): Result<List<User>> {
        return if (shouldReturnError) {
            Result.failure(Exception("Failed to fetch users"))
        } else {
            Result.success(
                listOf(
                    User(1, "Fake Alice", "fake_alice@test.com"),
                    User(2, "Fake Bob", "fake_bob@test.com")
                )
            )
        }
    }
}

三、ViewModel层

UserViewModel.kt,通过Hilt注入Repository,处理UI状态:

import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
import dagger.hilt.android.lifecycle.HiltViewModel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.launch
import javax.inject.Inject

@HiltViewModel
class UserViewModel @Inject constructor(
    private val userRepository: UserRepository
) : ViewModel() {

    private val _uiState = MutableStateFlow(UserUiState.Loading)
    val uiState: StateFlow<UserUiState> = _uiState

    init {
        fetchUsers()
    }

    fun fetchUsers() {
        viewModelScope.launch {
            _uiState.value = UserUiState.Loading
            val result = userRepository.getUsers()
            _uiState.value = when (result) {
                is Result.Success -> UserUiState.Success(result.data)
                is Result.Failure -> UserUiState.Error(result.exception.message ?: "Unknown error")
            }
        }
    }
}

sealed class UserUiState {
    object Loading : UserUiState()
    data class Success(val users: List<User>) : UserUiState()
    data class Error(val message: String) : UserUiState()
}

四、Compose UI层

UserScreen.kt,根据ViewModel的状态渲染UI:

import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.foundation.lazy.items
import androidx.compose.material3.Button
import androidx.compose.material3.Card
import androidx.compose.material3.CardDefaults
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.collectAsState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import androidx.lifecycle.viewmodel.compose.viewModel

@Composable
fun UserScreen(
    viewModel: UserViewModel = viewModel()
) {
    val uiState = viewModel.uiState.collectAsState()

    Column(
        modifier = Modifier.fillMaxSize(),
        horizontalAlignment = Alignment.CenterHorizontally,
        verticalArrangement = Arrangement.Center
    ) {
        when (val state = uiState.value) {
            is UserUiState.Loading -> {
                Text(text = "Loading users...")
            }
            is UserUiState.Success -> {
                LazyColumn(
                    modifier = Modifier.fillMaxSize().padding(16.dp),
                    verticalArrangement = Arrangement.spacedBy(12.dp)
                ) {
                    items(state.users) { user ->
                        UserCard(user = user)
                    }
                }
            }
            is UserUiState.Error -> {
                Column(horizontalAlignment = Alignment.CenterHorizontally) {
                    Text(text = state.message)
                    Button(onClick = { viewModel.fetchUsers() }) {
                        Text(text = "Retry")
                    }
                }
            }
        }
    }
}

@Composable
fun UserCard(user: User) {
    Card(
        modifier = Modifier.fillMaxWidth(),
        elevation = CardDefaults.cardElevation(defaultElevation = 4.dp)
    ) {
        Column(modifier = Modifier.padding(16.dp)) {
            Text(text = "Name: ${user.name}", modifier = Modifier.padding(bottom = 4.dp))
            Text(text = "Email: ${user.email}")
        }
    }
}

五、Hilt依赖注入配置

1. Application类

MyApplication.kt,标注Hilt应用:

import android.app.Application
import dagger.hilt.android.HiltAndroidApp

@HiltAndroidApp
class MyApplication : Application()

2. 生产环境Module

AppModule.kt,提供真实Repository:

import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import javax.inject.Singleton

@Module
@InstallIn(SingletonComponent::class)
abstract class AppModule {

    @Binds
    @Singleton
    abstract fun bindUserRepository(impl: UserRepositoryImpl): UserRepository
}

3. 测试环境Module

TestAppModule.kt,提供Fake Repository,用于替换真实实现:

import dagger.Module
import dagger.Provides
import dagger.hilt.InstallIn
import dagger.hilt.components.SingletonComponent
import dagger.hilt.testing.TestInstallIn
import javax.inject.Singleton

@TestInstallIn(
    components = [SingletonComponent::class],
    replaces = [AppModule::class]
)
@Module
object TestAppModule {

    @Provides
    @Singleton
    fun provideFakeUserRepository(): UserRepository {
        return FakeUserRepository()
    }
}

六、测试代码

1. ViewModel测试

UserViewModelTest.kt,测试ViewModel的逻辑:

import app.cash.turbine.test
import com.example.hiltcomposeexample.FakeUserRepository
import com.example.hiltcomposeexample.UserUiState
import com.example.hiltcomposeexample.UserViewModel
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.resetMain
import kotlinx.coroutines.test.runTest
import kotlinx.coroutines.test.setMain
import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject

@HiltAndroidTest
@UninstallModules(AppModule::class)
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {

    @get:Rule
    val hiltRule = HiltAndroidRule(this)

    @Inject
    lateinit var fakeRepository: FakeUserRepository

    @Inject
    lateinit var viewModel: UserViewModel

    @Before
    fun setUp() {
        hiltRule.inject()
        Dispatchers.setMain(StandardTestDispatcher())
    }

    @After
    fun tearDown() {
        Dispatchers.resetMain()
    }

    @Test
    fun `fetchUsers returns success state`() = runTest {
        viewModel.uiState.test {
            // 初始状态为Loading
            assert(awaitItem() is UserUiState.Loading)
            // 然后返回Success
            val successState = awaitItem() as UserUiState.Success
            assert(successState.users.size == 2)
            assert(successState.users[0].name == "Fake Alice")
            cancelAndIgnoreRemainingEvents()
        }
    }

    @Test
    fun `fetchUsers returns error state when repository fails`() = runTest {
        fakeRepository.shouldReturnError = true

        viewModel.uiState.test {
            assert(awaitItem() is UserUiState.Loading)
            val errorState = awaitItem() as UserUiState.Error
            assert(errorState.message == "Failed to fetch users")
            cancelAndIgnoreRemainingEvents()
        }
    }
}

2. UI测试

UserScreenTest.kt,测试Compose UI的渲染:

import androidx.compose.ui.test.assertIsDisplayed
import androidx.compose.ui.test.junit4.createAndroidComposeRule
import androidx.compose.ui.test.onNodeWithText
import androidx.compose.ui.test.performClick
import com.example.hiltcomposeexample.FakeUserRepository
import com.example.hiltcomposeexample.MainActivity
import com.example.hiltcomposeexample.UserScreen
import dagger.hilt.android.testing.HiltAndroidRule
import dagger.hilt.android.testing.HiltAndroidTest
import dagger.hilt.android.testing.UninstallModules
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import javax.inject.Inject

@HiltAndroidTest
@UninstallModules(AppModule::class)
class UserScreenTest {

    @get:Rule(order = 0)
    val hiltRule = HiltAndroidRule(this)

    @get:Rule(order = 1)
    val composeTestRule = createAndroidComposeRule<MainActivity>()

    @Inject
    lateinit var fakeRepository: FakeUserRepository

    @Before
    fun setUp() {
        hiltRule.inject()
        composeTestRule.setContent {
            UserScreen()
        }
    }

    @Test
    fun `loading state displays initially`() {
        composeTestRule.onNodeWithText("Loading users...").assertIsDisplayed()
    }

    @Test
    fun `success state displays user list`() {
        // 等待数据加载完成
        composeTestRule.waitUntil(2000) {
            composeTestRule.onNodeWithText("Name: Fake Alice").exists()
        }
        composeTestRule.onNodeWithText("Name: Fake Alice").assertIsDisplayed()
        composeTestRule.onNodeWithText("Email: fake_bob@test.com").assertIsDisplayed()
    }

    @Test
    fun `error state displays message and retry button`() {
        fakeRepository.shouldReturnError = true
        // 重新触发请求
        composeTestRule.runOnUiThread {
            composeTestRule.activity.viewModelStore.clear()
        }
        composeTestRule.setContent {
            UserScreen()
        }

        composeTestRule.waitUntil(2000) {
            composeTestRule.onNodeWithText("Failed to fetch users").exists()
        }
        composeTestRule.onNodeWithText("Failed to fetch users").assertIsDisplayed()
        composeTestRule.onNodeWithText("Retry").assertIsDisplayed()

        // 点击重试,验证是否回到加载状态
        composeTestRule.onNodeWithText("Retry").performClick()
        composeTestRule.onNodeWithText("Loading users...").assertIsDisplayed()
    }
}

3. MainActivity

import androidx.appcompat.app.AppCompatActivity
import android.os.Bundle
import androidx.activity.compose.setContent
import com.example.hiltcomposeexample.ui.theme.HiltComposeExampleTheme

class MainActivity : AppCompatActivity() {
    override fun onCreate(savedInstanceState: Bundle?) {
        super.onCreate(savedInstanceState)
        setContent {
            HiltComposeExampleTheme {
                UserScreen()
            }
        }
    }
}

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.13 16:22:02