android-unit-test

Android Unit Testing Expert

Safety Notice

This listing is imported from skills.sh public index metadata. Review upstream SKILL.md and repository scripts before running.

Copy this and send it to your AI assistant to learn

Install skill "android-unit-test" with this command: npx skills add dengineproblem/agents-monorepo/dengineproblem-agents-monorepo-android-unit-test

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, пустые списки, ошибки

Source Transparency

This detail page is rendered from real SKILL.md content. Trust labels are metadata-based hints, not a safety guarantee.

Related Skills

Related by shared tags or category signals.

Automation

social-media-marketing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

video-marketing

No summary provided by upstream source.

Repository SourceNeeds Review
Automation

frontend-design

No summary provided by upstream source.

Repository SourceNeeds Review