| name | android-unit-test |
| description | Эксперт Android тестирования. Используй для JUnit, Espresso и Android test patterns. |
Android Unit Testing Expert
Эксперт по тестированию Android приложений с использованием JUnit 5, Mockito и Kotlin.
Настройка зависимостей
// 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 Pattern (Arrange, Act, Assert)
class UserRepositoryTest {
@Test
fun `getUser returns user when found`() {
// Arrange
val userId = "user_123"
val expectedUser = User(userId, "John Doe", "john@example.com")
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 Testing
@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", "john@example.com")
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 Testing
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", "john@example.com"))
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", "john@example.com"))
whenever(localDataSource.getUsers()).thenReturn(emptyList())
whenever(remoteDataSource.getUsers()).thenReturn(remoteUsers)
// Act
val result = repository.getUsers()
// Assert
assertEquals(remoteUsers, result)
verify(localDataSource).saveUsers(remoteUsers)
}
}
Flow Testing с Turbine
@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()))
)
}
}
MockK для Kotlin
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", "john@example.com")
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", "john@example.com")
coEvery { api.getUser("1") } returns user
coEvery { cache.save(any()) } just Runs
service.getUser("1")
coVerifyOrder {
api.getUser("1")
cache.save(user)
}
}
}
Test Data Factories
object UserFactory {
fun create(
id: String = "user_${UUID.randomUUID()}",
name: String = "Test User",
email: String = "test@example.com",
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)
}
Custom Assertions
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 Testing
@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("test@example.com"))
onView(withId(R.id.passwordInput)).perform(typeText("password123"))
onView(withId(R.id.loginButton)).perform(click())
onView(withId(R.id.homeScreen)).check(matches(isDisplayed()))
}
}
Лучшие практики
- Один тест = одно поведение — каждый тест проверяет одну вещь
- Описательные имена — используйте backticks для читаемых названий
- AAA паттерн — Arrange, Act, Assert в каждом тесте
- Изолированность — тесты не зависят друг от друга
- Быстрота — unit тесты должны выполняться мгновенно
- Тестируйте edge cases — null, пустые списки, ошибки