Комментарии (1)
Ответ сгенерирован нейросетью и может содержать ошибки
Тестирование ПО — полное руководство
Тестирование ПО — это процесс проверки приложения на соответствие требованиям и выявления дефектов перед выпуском. Это критически важная часть разработки, которая обеспечивает качество, надежность и безопасность приложения.
Основное определение
Тестирование ПО — это систематическое исследование приложения с целью:
- Убедиться что оно работает корректно
- Найти ошибки (баги)
- Убедиться в соответствии требованиям
- Оценить качество и производительность
Зачем тестирование нужно
Выявление дефектов — ловим баги до пользователей
Обеспечение качества — гарантируем что приложение соответствует стандартам
Снижение затрат — баг найденный на этапе разработки стоит дешевле чем в продакшене
Уверенность — возможность рефакторить и добавлять фичи без страха сломать существующий функционал
Документация — тесты документируют как код должен работать
Уровни тестирования
// 1. Unit Testing — тестирование отдельных функций/классов
@Test
fun testUserNameValidation() {
val validator = UserValidator()
assertTrue(validator.isValidName("John"))
assertFalse(validator.isValidName(""))
}
// 2. Integration Testing — тестирование взаимодействия компонентов
@Test
fun testUserRepositoryFetchesFromAPI() = runTest {
val mockApi = MockUserApi()
val repository = UserRepository(mockApi)
val users = repository.getUsers()
assertEquals(2, users.size)
}
// 3. System Testing — тестирование всей системы
// Проверяем что User flow работает: вход -> список -> детали -> выход
// 4. Acceptance Testing — проверка требований бизнеса
// Пользователь может создать профиль, загрузить фото, добавить друзей
Типы тестирования
Functional Testing — проверяет функциональность (что работает)
@Test
fun testLoginWithCorrectCredentials() {
val viewModel = LoginViewModel(mockAuthService)
viewModel.login("user@example.com", "password123")
assertEquals(LoginState.Success, viewModel.state.value)
}
Performance Testing — проверяет скорость и оптимизацию
@Test
fun testLargeListRenderingPerformance() {
val items = (1..10000).map { Item(it) }
val startTime = System.currentTimeMillis()
renderList(items)
val duration = System.currentTimeMillis() - startTime
assertTrue(duration < 100, "Rendering took too long: $duration ms")
}
Security Testing — проверяет безопасность
@Test
fun testPasswordIsEncrypted() {
val password = "secret123"
val encrypted = PasswordUtils.encrypt(password)
assertNotEquals(password, encrypted)
assertTrue(PasswordUtils.verify(password, encrypted))
}
Usability Testing — проверяет удобство интерфейса (обычно ручное)
Compatibility Testing — проверяет работу на разных устройствах
Android Testing Pyramid
/\
/ \ UI Tests (Espresso)
/ \
/------\
/ Integration Tests \
/ (instrumented) \
/------------------------\ Unit Tests (JUnit)
Unit Tests (70%) — быстрые, запускаются без устройства
class UserViewModelTest {
@Test
fun testFetchUserData() = runTest {
val mockRepository = mockk<UserRepository>()
coEvery { mockRepository.getUser(1) } returns User(1, "John")
val viewModel = UserViewModel(mockRepository)
viewModel.fetchUser(1)
assertEquals("John", viewModel.user.value?.name)
}
}
Integration Tests (20%) — тестируют взаимодействие нескольких компонентов
@RunWith(AndroidRunner::class)
class UserRepositoryIntegrationTest {
@get:Rule
val instantExecutorRule = InstantTaskExecutorRule()
@Test
fun testDatabasePersistence() = runTest {
val database = Room.inMemoryDatabaseBuilder(
InstrumentationRegistry.getInstrumentation().context,
AppDatabase::class.java
).build()
val repository = UserRepository(database.userDao())
repository.insertUser(User(1, "John"))
val users = repository.getUsers()
assertEquals(1, users.size)
}
}
UI Tests (10%) — медленные, запускаются на устройстве
class UserListScreenTest {
@get:Rule
val composeTestRule = createComposeRule()
@Test
fun testUserListIsDisplayed() {
composeTestRule.setContent {
UserListScreen()
}
composeTestRule.onNodeWithText("John").assertIsDisplayed()
composeTestRule.onNodeWithTag("user_list").onChildren()
.assertCountEquals(2)
}
}
Test-Driven Development (TDD)
// Шаг 1: Написать падающий тест (RED)
@Test
fun testCalculateDiscount() {
val calculator = DiscountCalculator()
val result = calculator.calculate(100, 20) // 20% скидка
assertEquals(80, result)
}
// Шаг 2: Написать минимальный код (GREEN)
class DiscountCalculator {
fun calculate(price: Int, discount: Int): Int {
return price - (price * discount / 100)
}
}
// Шаг 3: Рефакторить (REFACTOR)
class DiscountCalculator {
fun calculate(price: Int, discountPercent: Int): Int {
val discountAmount = price * discountPercent / 100
return price - discountAmount
}
fun calculateWithTax(price: Int, discountPercent: Int, taxPercent: Int): Int {
val afterDiscount = calculate(price, discountPercent)
val tax = afterDiscount * taxPercent / 100
return afterDiscount + tax
}
}
Best Practices для тестирования
Arrange-Act-Assert паттерн — структурируй тесты ясно
@Test
fun testUserLogin() {
// Arrange — подготовка
val mockAuthService = mockk<AuthService>()
coEvery { mockAuthService.login("user", "pass") } returns User(1, "user")
val viewModel = LoginViewModel(mockAuthService)
// Act — выполнение
viewModel.login("user", "pass")
// Assert — проверка
assertEquals(LoginState.Success, viewModel.state.value)
}
Один assert на тест — тест проверяет одно поведение
// Плохо
@Test
fun testUser() {
val user = createUser()
assertEquals("John", user.name) // Слишком много проверок
assertEquals(25, user.age)
assertTrue(user.isActive)
}
// Хорошо
@Test
fun testUserNameIsJohn() {
val user = createUser()
assertEquals("John", user.name)
}
Используй мокирование — для изоляции тестов
@Test
fun testRepositoryFetchesFromCache() = runTest {
val mockApi = mockk<UserApi>()
val cache = FakeCache()
val repository = UserRepository(mockApi, cache)
// Пользователь уже в кэше
cache.put("user_1", User(1, "John"))
val user = repository.getUser(1)
// API не должна быть вызвана
coVerify(exactly = 0) { mockApi.fetchUser(1) }
assertEquals("John", user.name)
}
Достаточное покрытие — стремись к 80-90% test coverage
# Проверить coverage
./gradlew test --info
./gradlew jacocoTestReport
Инструменты для тестирования Android
| Инструмент | Назначение |
|---|---|
| JUnit | Unit тесты |
| Mockito/MockK | Мокирование |
| Espresso | UI тесты |
| Robolectric | Unit тесты с Android компонентами |
| Room | Тестирование БД |
| Coroutines Test | Тестирование корутин |
| Turbine | Тестирование Flow |
Типичный workflow
1. Разработчик пишет код
2. Пишет тесты для кода
3. Запускает тесты локально (./gradlew test)
4. Отправляет Pull Request
5. CI/CD запускает все тесты автоматически
6. Если 90%+ pass → merge
7. Если есть падающие → правит разработчик
Заключение
Тестирование ПО — это инвестиция в качество. Да, писать тесты дольше, но они:
- Экономят время на отладку
- Предотвращают регрессию
- Дают уверенность при рефакторинге
- Служат документацией
- Снижают затраты на поддержку
Хороший разработчик пишет не только код, но и тесты для него. "Code without tests is legacy code" — Robert C. Martin