请求提供含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
相关产品推荐
相关产品推荐

