Unit-тестирование — метод проверки программного обеспечения, при котором тестируется корректность работы отдельных модулей или функций кода в изоляции от остальной системы. По данным Martin Fowler, 2026, юнит-тесты являются фундаментом CI/CD и рефакторинга, обеспечивая быструю обратную связь о работоспособности кода. Модульное тестирование помогает обнаруживать ошибки на ранних этапах разработки, снижая стоимость их исправления в десятки раз.
Главное
Unit-тестирование — это процесс проверки отдельных единиц (unit) исходного кода — функций, методов, классов — в изоляции от остальной части программы. Каждый тест запускает конкретный сценарий использования модуля и проверяет, что результат соответствует ожидаемому. Unit-тесты пишутся на том же языке программирования, что и основной код, и выполняются автоматически в среде разработки или в CI/CD пайплайне. В отличие от интеграционных тестов, юнит-тесты не взаимодействуют с реальными базами данных, файловой системой или сетевыми сервисами.
Основная цель — быстрая обратная связь о корректности кода после изменений. Если разработчик рефакторит метод, набор unit-тестов подтверждает, что поведение не сломалось. По данным Google Testing Blog (2025), проекты с покрытием юнит-тестами выше 60% имеют в 2.5 раза меньше production-инцидентов. Дополнительные преимущества: документация кода (тесты показывают, как использовать API), упрощение рефакторинга (можно менять реализацию, сохраняя поведение) и быстрая диагностика регрессий.
Не всякий автоматический тест — unit-тест. Критерии: тестируется один модуль (класс или функция), внешние зависимости заменены моками или стабами, тест выполняется за миллисекунды, не требует поднятия сервера или базы данных. Тест, обращающийся к реальной базе данных — это интеграционный тест. Тест, открывающий браузер — E2E-тест. Понимание границ между типами тестов важно для правильного распределения усилий в пирамиде тестирования.
Качественные unit-тесты следуют принципам FIRST, сформулированным Robert C. Martin. Каждый тест должен быть Fast (быстрый — миллисекунды), Isolated (изолированный — не зависит от других тестов), Repeatable (воспроизводимый — одинаковый результат на любой машине), Self-validating (самопроверяемый — результат "passed" или "failed", без ручной проверки) и Timely (своевременный — написан до или одновременно с кодом). Нарушение любого принципа снижает ценность теста.
Стандартный шаблон для написания unit-тестов. Arrange — подготовка данных и зависимостей: создание объектов, настройка моков, задание входных параметров. Act — выполнение тестируемого действия: вызов метода или функции. Assert — проверка результата: сравнение фактического значения с ожидаемым. Разделение на три блока делает тест читаемым и понятным. Если блок Assert требует сложной логики — тест, вероятно, проверяет слишком много за один раз.
// Пример unit-теста по шаблону AAA на Kotlin с JUnit 5
class CalculatorTest {
private lateinit var calculator: Calculator
@BeforeEach
fun setUp() {
// ARRANGE — создаём тестируемый объект
calculator = Calculator()
}
@Test
fun addition_shouldReturnCorrectSum() {
// ACT — выполняем действие
val result = calculator.add(2, 3)
// ASSERT — проверяем результат
Assertions.assertEquals(5, result)
}
}
Имя теста должно описывать, что проверяется и какой ожидается результат. Формат: [methodName]_[scenario]_[expectedResult]. Пример: calculateTotal_whenDiscountApplied_shouldReturnDiscountedTotal. Хорошее имя теста заменяет комментарий и при падении сразу указывает, какая функциональность нарушена. Избегайте имён вроде test1, checkSomething или verify — они не несут информации и затрудняют диагностику.
Для изоляции тестируемого модуля от внешних зависимостей используются тестовые дублёры (test doubles). Основные типы: моки (mocks) — проверяют, что определённый метод был вызван с нужными параметрами; стабы (stubs) — возвращают заданные значения при вызове метода; фейки (fakes) — упрощённая реализация реального компонента (например, InMemoryUserRepository вместо UserRepository, работающего с БД). Выбор зависит от того, что нужно проверить: состояние (стаб) или взаимодействие (мок).
| Дублёр | Что проверяет | Пример |
|---|---|---|
| Мок | Вызов метода с правильными параметрами | userRepository.save(user) был вызван ровно 1 раз |
| Стаб | Возвращаемое значение | repository.findById(1) возвращает User(id=1, name="Test") |
| Фейк | Логика через упрощённую реализацию | InMemoryMapUserRepository с HashMap вместо БД |
| Шпион | Частичное мокинг реального объекта | spy(repo).when(findById).thenReturn(user) |
Mockito — самый популярный фреймворк для мокирования в Java и Kotlin. Позволяет создавать моки через mock(), настраивать возврат значений через when().thenReturn() и проверять вызовы через verify(). Современные версии Mockito (5.x) поддерживают статические моки (mockStatic) и упрощённый синтаксис через BDDMockito (given-willReturn). Важное правило: не мокайте то, что не ваше — не создавайте моки для value-объектов и стандартных библиотек.
// Пример unit-теста с Mockito на Kotlin
class OrderServiceTest {
@Mock
private lateinit var paymentGateway: PaymentGateway
@Mock
private lateinit var userRepository: UserRepository
private lateinit var orderService: OrderService
@BeforeEach
fun init() {
MockitoAnnotations.openMocks(this)
orderService = OrderService(paymentGateway, userRepository)
}
@Test
fun processOrder_whenPaymentFails_shouldThrowException() {
// given
val user = User(id = 1, balance = 100.0)
val order = Order(amount = 200.0)
Mockito.`when`(paymentGateway.charge(any())).thenReturn(false)
Mockito.`when`(userRepository.findById(1)).thenReturn(user)
// when & then
assert Throws<PaymentException> {
orderService.processOrder(user.id, order)
}
// verify
Mockito.verify(paymentGateway).charge(any())
}
}
TDD (Test-Driven Development) — методология, при которой тест пишется до реализации кода. Цикл "Red-Green-Refactor": написать тест, который падает (Red), написать минимальный код для прохождения теста (Green), улучшить код без изменения поведения (Refactor). TDD гарантирует, что весь код покрыт тестами (coverage = 100% для написанного функционала) и что код тестируем — если код сложно протестировать, значит, архитектура требует улучшения.
По данным исследования IBM (2006-2026, longitudinal study), команды, использующие TDD, допускают на 40-80% меньше дефектов в production по сравнению с командами, пишущими тесты после кода. TDD также улучшает архитектуру: разработчик вынужден думать о дизайне API до реализации, что приводит к слабой связанности (loose coupling) и высокой связности (high cohesion). Дополнительный эффект — документация живым кодом: тесты служат спецификацией поведения модуля, которая всегда актуальна.
TDD не всегда оптимален. UI-компоненты сложно тестировать в изоляции — для них эффективнее snapshot-тесты или визуальное регрессионное тестирование (Percy, Chromatic). Прототипирование и исследования (spike solutions) не требуют тестов. Legacy-код без тестов сложно покрыть через TDD — здесь сначала нужны characterization tests (тесты, фиксирующие текущее поведение перед рефакторингом). В этих случаях TDD не отменяется полностью, а адаптируется — пишутся тесты на изменяемый функционал, а не на весь legacy-код.
Мобильная разработка имеет специфику: бизнес-логика часто смешана с UI-кодом (Activity, ViewController, ViewModel), что усложняет unit-тестирование. Лучшая практика — тонкие View, толстые ViewModel: выносите всю логику из UI-компонентов в отдельные классы (UseCase, Repository, ViewModel), которые легко тестировать без эмулятора. Для Android и iOS существуют нативные фреймворки unit-тестирования, работающие на JVM/Native без запуска устройства.
Android unit-тесты выполняются на локальной JVM без эмулятора, что обеспечивает скорость выполнения — типичный тест занимает < 100ms. JUnit 5 — основной runner. Для ViewModel-тестов используйте kotlinx-coroutines-test для тестирования корутин и Turbine для тестирования StateFlow. Robolectric позволяет тестировать Android-зависимые компоненты (Context, Resources) без эмулятора, загружая shadow-классы. Для Compose-тестов используйте Compose UI Test — но это уже UI-тесты, не unit.
iOS unit-тесты пишутся на Swift с XCTest (встроен в Xcode). Quick + Nimble — BDD-фреймворки для более читаемых тестов (describe/context/it). Для мокирования используйте Cuckoo (генерация моков) или SwiftyMocky. Swift поддерживает протоколы и dependency injection, что облегчает замену зависимостей. Key point: iOS unit-тесты выполняются на симуляторе macOS, а не на реальном устройстве. Тесты, требующие аппаратных функций (камера, Bluetooth), — это интеграционные тесты.
Flutter unit-тесты используют пакет flutter_test и выполняются на Dart VM без эмулятора. Для мокирования — пакет mockito с генератором кода (build_runner). Widget-тесты (в том же пакете) тестируют отдельные виджеты, но требуют рендеринга и работают медленнее — используйте их только для проверки UI-логики. Чистая Dart-логика (модели, репозитории, блобы) тестируется как обычные Dart-тесты без импорта flutter_test.
// Пример unit-теста на Flutter с mockito
import 'package:flutter_test/flutter_test.dart';
import 'package:mockito/mockito.dart';
import 'package:mockito/annotations.dart';
@GenerateMocks([ApiClient])
import 'user_repository_test.mocks.dart';
void main() {
late MockApiClient mockApi;
late UserRepository repository;
setUp(() {
mockApi = MockApiClient();
repository = UserRepository(mockApi);
});
test('fetchUser returns user when API succeeds', () async {
// Arrange
final expectedUser = User(id: 1, name: 'Test');
when(mockApi.getUser(1))
.thenAnswer((_) async => expectedUser);
// Act
final result = await repository.fetchUser(1);
// Assert
expect(result, expectedUser);
verify(mockApi.getUser(1)).called(1);
});
}
Эффективное unit-тестирование требует дисциплины. Главное правило: тестируйте поведение, а не реализацию. Тест не должен знать, как модуль реализован внутри (какие приватные методы вызываются, в каком порядке). Если тест привязан к реализации, он ломается при каждом рефакторинге и теряет ценность. Тест проверяет контракт: при входе X должен быть выход Y. Исключение — тесты для алгоритмов с критической производительностью, где важна последовательность вызовов.
100% coverage — недостижимая и не нужная цель. По данным Google Testing Blog (2025), оптимальный уровень покрытия для unit-тестов — 70-80% строк кода. 100% coverage часто достигается за счёт тестирования геттеров, сеттеров и конструкторов, что не приносит ценности. Фокусируйтесь на критической бизнес-логике: сложные вычисления, валидация, обработка ошибок, edge-cases. Используйте JaCoCo (Java), Coverage.py (Python), Istanbul (JS) для измерения и настройте порог в CI — падение сборки при coverage < 60%.
Unit-тесты — первая стадия любого CI/CD пайплайна. Они выполняются при каждом пуше в репозиторий, до сборки и деплоя. Среднее время прогона unit-тестов в проекте не должно превышать 5 минут — если дольше, тесты перестают быть "быстрыми" и разработчики перестают их запускать локально. Разделяйте тесты на быстрые (unit) и медленные (integration) и запускайте их в разных стейджах пайплайна. Используйте parallel execution и fail-fast для ускорения.
Часто задаваемые вопросы
Unit-тест проверяет один модуль в изоляции, заменяя внешние зависимости моками. Интеграционный тест проверяет взаимодействие между несколькими реальными компонентами (БД, API, файловая система). Unit-тесты выполняются за миллисекунды, интеграционные — за секунды. В пирамиде тестирования unit-тесты занимают 70%.
Выбор зависит от платформы: JUnit 5 для Java/Kotlin, XCTest для iOS/Swift, pytest для Python, Jest/Vitest для JavaScript/TypeScript, flutter_test для Flutter. Для мокирования используйте Mockito (Java), Cuckoo (iOS), unittest.mock (Python) или vitest.mock (JS). Все современные фреймворки поддерживают параметризованные тесты, встроенные assertions и параллельный запуск.
Fast — тест выполняется за миллисекунды. Isolated — не зависит от других тестов и внешних систем. Repeatable — даёт одинаковый результат на любой машине. Self-validating — проверяет результат автоматически. Timely — написан до или синхронно с кодом. Нарушение хотя бы одного принципа снижает эффективность тестирования.
Да, обязательно. ViewModel содержит бизнес-логику — обработку событий, трансформацию данных, управление состоянием. На Android используйте kotlinx-coroutines-test для корутин и Turbine для тестирования StateFlow. На iOS тестируйте Combine Publishers или async/await в ViewModel. ViewModel-тесты — это чистые unit-тесты, работающие на JVM/macOS без эмулятора.
Сетевые запросы в unit-тестах не выполняются — они заменяются моками HTTP-клиента. На Android используйте MockWebServer (OkHttp) — он поднимает локальный HTTP-сервер, что предпочтительнее моков, так как воспроизводит реальное сетевое взаимодействие. MockWebServer даёт изоляцию без потери реалистичности. Для iOS — OHHTTPStubs или URLProtocol для перехвата и подмены ответов.
Итоги
Мы разработаем мобильное приложение под ключ
IT Sectr создаёт приложения для iOS и Android для стартапов и бизнеса с 2017 года. Мы проконсультируем вас и предложим наилучшее решение.
Читайте также