关于 android-unit-test
面向 Android 开发者的单元测试技能,涵盖 Kotlin 测试示例与常用测试模式,也包括 ViewModel、Repository 和 UI 测试。
主要功能
- 01JUnit、Mockito 与 MockK 测试
- 02AAA 测试模式
- 03ViewModel 与 Repository 测试
- 04Flow 与 Turbine 测试
- 05参数化测试和测试数据工厂
- 06Espresso UI 测试
适用场景
- 01为 Kotlin 代码编写单元测试
- 02验证 ViewModel 与 Repository 行为
- 03测试 Flow 发出的数据
- 04使用 Espresso 检查 Android 界面
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ dengineproblem/ agents-monorepo --skill android-unit-test安装后在 Claude Code 中输入/android-unit-test确认已加载。
详细介绍
Android 单元测试专家
使用 JUnit 5、Mockito 和 Kotlin 测试 Android 应用的专家。
配置依赖项
// build.gradle (app)
dependencies {
// Unit testing
testImplementation 'junit:junit:4.13.2'
testImplementation 'org.junit.jupiter:junit-jupiter:5.9.3'
testImplementation 'org.mockito:mockito-core:5.3.1'
testImplementation 'org.mockito.kotlin:mockito-kotlin:5.0.0'
testImplementation 'io.mockk:mockk:1.13.5'
testImplementation 'org.jetbrains.kotlinx:kotlinx-coroutines-test:1.7.1'
testImplementation 'app.cash.turbine:turbine:1.0.0'
// Instrumentation testing
androidTestImplementation 'androidx.test.ext:junit:1.1.5'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.5.1'
androidTestImplementation 'androidx.test:runner:1.5.2'
androidTestImplementation 'androidx.test:rules:1.5.0'
}
AAA 模式(Arrange、Act、Assert)
class UserRepositoryTest {
@Test
fun `getUser returns user when found`() {
// Arrange
val userId = "user_123"
val expectedUser = User(userId, "John Doe", "[email protected]")
val mockDataSource = mock<UserDataSource> {
on { getUser(userId) } doReturn expectedUser
}
val repository = UserRepository(mockDataSource)
// Act
val result = repository.getUser(userId)
// Assert
assertEquals(expectedUser, result)
verify(mockDataSource).getUser(userId)
}
}
ViewModel 测试
@OptIn(ExperimentalCoroutinesApi::class)
class UserViewModelTest {
@get:Rule
val mainDispatcherRule = MainDispatcherRule()
private lateinit var viewModel: UserViewModel
private lateinit var repository: UserRepository
@Before
fun setup() {
repository = mock()
viewModel = UserViewModel(repository)
}
@Test
fun `loadUser updates state to success when repository returns user`() = runTest {
// Arrange
val user = User("1", "John", "[email protected]")
whenever(repository.getUser("1")).thenReturn(Result.success(user))
// Act
viewModel.loadUser("1")
// Assert
val state = viewModel.uiState.value
assertTrue(state is UserUiState.Success)
assertEquals(user, (state as UserUiState.Success).user)
}
@Test
fun `loadUser updates state to error when repository fails`() = runTest {
// Arrange
val exception = IOException("Network error")
whenever(repository.getUser("1")).thenReturn(Result.failure(exception))
// Act
viewModel.loadUser("1")
// Assert
val state = viewModel.uiState.value
assertTrue(state is UserUiState.Error)
assertEquals("Network error", (state as UserUiState.Error).message)
}
}
MainDispatcherRule
@OptIn(ExperimentalCoroutinesApi::class)
class MainDispatcherRule(
private val testDispatcher: TestDispatcher = UnconfinedTestDispatcher()
) : TestWatcher() {
override fun starting(description: Description) {
Dispatchers.setMain(testDispatcher)
}
override fun finished(description: Description) {
Dispatchers.resetMain()
}
}
Repository 测试
class UserRepositoryTest {
private lateinit var repository: UserRepository
private lateinit var remoteDataSource: UserRemoteDataSource
private lateinit var localDataSource: UserLocalDataSource
@Before
fun setup() {
remoteDataSource = mock()
localDataSource = mock()
repository = UserRepository(remoteDataSource, localDataSource)
}
@Test
fun `getUsers returns cached data when available`() = runTest {
// Arrange
val cachedUsers = listOf(User("1", "John", "[email protected]"))
whenever(localDataSource.getUsers()).thenReturn(cachedUsers)
// Act
val result = repository.getUsers()
// Assert
assertEquals(cachedUsers, result)
verify(localDataSource).getUsers()
verifyNoInteractions(remoteDataSource)
}
@Test
fun `getUsers fetches from remote when cache is empty`() = runTest {
// Arrange
val remoteUsers = listOf(User("1", "John", "[email protected]"))
whenever(localDataSource.getUsers()).thenReturn(emptyList())
whenever(remoteDataSource.getUsers()).thenReturn(remoteUsers)
// Act
val result = repository.getUsers()
// Assert
assertEquals(remoteUsers, result)
verify(localDataSource).saveUsers(remoteUsers)
}
}
使用 Turbine 测试 Flow
@OptIn(ExperimentalCoroutinesApi::class)
class FlowTestExample {
@Test
fun `userFlow emits expected values`() = runTest {
val repository = UserRepository()
repository.userFlow.test {
// Initial state
assertEquals(UserState.Loading, awaitItem())
// After loading
assertEquals(UserState.Success(user), awaitItem())
// Cancel and ensure no more emissions
cancelAndIgnoreRemainingEvents()
}
}
@Test
fun `searchFlow debounces and emits results`() = runTest {
val viewModel = SearchViewModel()
viewModel.searchResults.test {
viewModel.onSearchQueryChanged("test")
advanceTimeBy(300) // Debounce time
val result = awaitItem()
assertTrue(result.isNotEmpty())
cancelAndIgnoreRemainingEvents()
}
}
}
参数化测试
class CalculatorTest {
@ParameterizedTest
@CsvSource(
"1, 1, 2",
"2, 3, 5",
"10, -5, 5",
"0, 0, 0"
)
fun `add returns correct sum`(a: Int, b: Int, expected: Int) {
val calculator = Calculator()
assertEquals(expected, calculator.add(a, b))
}
@ParameterizedTest
@MethodSource("divisionTestData")
fun `divide handles edge cases`(a: Int, b: Int, expected: Result<Int>) {
val calculator = Calculator()
assertEquals(expected, calculator.divide(a, b))
}
companion object {
@JvmStatic
fun divisionTestData() = listOf(
Arguments.of(10, 2, Result.success(5)),
Arguments.of(9, 3, Result.success(3)),
Arguments.of(5, 0, Result.failure<Int>(ArithmeticException()))
)
}
}
用于 Kotlin 的 MockK
class UserServiceTest {
@MockK
private lateinit var api: UserApi
@MockK
private lateinit var cache: UserCache
private lateinit var service: UserService
@Before
fun setup() {
MockKAnnotations.init(this)
service = UserService(api, cache)
}
@Test
fun `getUser uses coEvery for suspend functions`() = runTest {
// Arrange
val user = User("1", "John", "[email protected]")
coEvery { api.getUser("1") } returns user
coEvery { cache.save(any()) } just Runs
// Act
val result = service.getUser("1")
// Assert
assertEquals(user, result)
coVerify { cache.save(user) }
}
@Test
fun `verify call order`() = runTest {
val user = User("1", "John", "[email protected]")
coEvery { api.getUser("1") } returns user
coEvery { cache.save(any()) } just Runs
service.getUser("1")
coVerifyOrder {
api.getUser("1")
cache.save(user)
}
}
}
测试数据工厂
object UserFactory {
fun create(
id: String = "user_${UUID.randomUUID()}",
name: String = "Test User",
email: String = "[email protected]",
isActive: Boolean = true
) = User(id, name, email, isActive)
fun createList(count: Int = 5) = (1..count).map {
create(id = "user_$it", name = "User $it")
}
}
// Usage in tests
@Test
fun `test with factory`() {
val user = UserFactory.create(name = "Custom Name")
val users = UserFactory.createList(10)
}
自定义断言
fun <T> Result<T>.shouldBeSuccess(): T {
assertTrue(this.isSuccess, "Expected success but was failure: ${this.exceptionOrNull()}")
return this.getOrThrow()
}
fun <T> Result<T>.shouldBeFailure(): Throwable {
assertTrue(this.isFailure, "Expected failure but was success: ${this.getOrNull()}")
return this.exceptionOrNull()!!
}
// Usage
@Test
fun `repository returns success`() {
val result = repository.getUser("1")
val user = result.shouldBeSuccess()
assertEquals("John", user.name)
}
Espresso UI 测试
@RunWith(AndroidJUnit4::class)
class LoginActivityTest {
@get:Rule
val activityRule = ActivityScenarioRule(LoginActivity::class.java)
@Test
fun loginButton_isDisabled_whenEmailIsEmpty() {
onView(withId(R.id.emailInput)).perform(clearText())
onView(withId(R.id.passwordInput)).perform(typeText("password123"))
onView(withId(R.id.loginButton)).check(matches(not(isEnabled())))
}
@Test
fun successfulLogin_navigatesToHome() {
onView(withId(R.id.emailInput)).perform(typeText("[email protected]"))
onView(withId(R.id.passwordInput)).perform(typeText("password123"))
onView(withId(R.id.loginButton)).perform(click())
onView(withId(R.id.homeScreen)).check(matches(isDisplayed()))
}
}
最佳实践
- 一个测试 = 一种行为 — 每个测试只验证一件事
- 使用描述性名称 — 使用反引号编写易读的名称
- AAA 模式 — 每个测试都遵循 Arrange、Act、Assert
- 相互隔离 — 测试之间互不依赖
- 快速执行 — 单元测试应瞬间完成
- 测试边界情况 — null、空列表、错误
评论
支持 Markdown