Claude-skill-registry android-unit-test
Эксперт Android тестирования. Используй для JUnit, Espresso и Android test patterns.
install
source · Clone the upstream repo
git clone https://github.com/majiayu000/claude-skill-registry
Claude Code · Install into ~/.claude/skills/
T=$(mktemp -d) && git clone --depth=1 https://github.com/majiayu000/claude-skill-registry "$T" && mkdir -p ~/.claude/skills && cp -r "$T/skills/data/android-unit-test" ~/.claude/skills/majiayu000-claude-skill-registry-android-unit-test && rm -rf "$T"
manifest:
skills/data/android-unit-test/SKILL.mdsource content
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, пустые списки, ошибки