Ответ
В моих Flutter-проектах для unit-тестирования я использую следующий стек:
Основные пакеты:
test/flutter_test: основной фреймворк для написания тестов и ассертов.mocktail: для создания моков зависимостей. Предпочитаю его из-за null-safety и отсутствия необходимости в кодогенерации.bloc_test: если в проекте используется BLoC, этот пакет значительно упрощает тестирование cubit'ов и bloc'ов.
Пример теста бизнес-логики (без UI):
import 'package:test/test.dart';
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
void main() {
group('Counter', () {
late Counter counter;
setUp(() {
// Инициализация перед каждым тестом
counter = Counter();
});
test('value should start at 0', () {
expect(counter.value, 0);
});
test('value should be 1 after increment', () {
counter.increment();
expect(counter.value, 1);
});
test('value should be -1 after decrement', () {
counter.decrement();
expect(counter.value, -1);
});
});
}
Пример теста с моком (mocktail):
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
// Репозиторий (зависимость)
abstract class UserRepository {
Future<String> fetchUserName(int id);
}
// Сервис, который тестируем
class UserService {
final UserRepository repository;
UserService(this.repository);
Future<String> getUserName(int id) async {
return 'User: ${await repository.fetchUserName(id)}';
}
}
// Мок репозитория
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late UserService userService;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
userService = UserService(mockRepository);
});
test('getUserName should format data from repository', () async {
// Настраиваем мок
when(() => mockRepository.fetchUserName(1))
.thenAnswer((_) async => 'John');
// Выполняем и проверяем
final result = await userService.getUserName(1);
expect(result, 'User: John');
// Проверяем, что метод был вызван с правильным аргументом
verify(() => mockRepository.fetchUserName(1)).called(1);
});
}
Ключевые принципы, которых я придерживаюсь:
- Изоляция: Каждый тест должен быть независимым и не полагаться на состояние, созданное другими тестами.
- Одна ответственность: Тест проверяет одну конкретную функциональность или сценарий.
- Читаемость: Использую
setUp/tearDownдля подготовки и очистки, аgroupдля логической группировки тестов. - Скорость: Unit-тесты должны выполняться быстро, без эмулятора/симулятора.
Ответ 18+ 🔞
А, ну ты про тесты спрашиваешь! Давай разжую, как я это обычно делаю, чтобы не было потом мучительно больно за говнокод, который на продакшене накрылся медным тазом.
Вот мой стандартный набор, без которого я как без рук. Главное — чтобы всё летало быстро, а не как эти UI-тесты, которые грузятся, будто на дворе 2002-й год.
Что у меня в арсенале всегда:
test/flutter_test: это основа основ, без них нихуя не напишешь. Там все ассерты и каркас.mocktail: вот это, блядь, вещь! Раньше мучился сmockitoи его кодогенерацией, а тут — чистый, null-safe, и ничего генерировать не надо. Просто наследуешься отMockи всё, красота.bloc_test: если в проекте BLoC, то это спасение. Тестировать cubit'ы и bloc'ы без него — это просто ебать копать, честное слово.
Вот смотри, простейший пример, как проверить какую-нибудь логику, которая с UI не связана:
import 'package:test/test.dart';
class Counter {
int value = 0;
void increment() => value++;
void decrement() => value--;
}
void main() {
group('Counter', () {
late Counter counter;
setUp(() {
// Перед каждым тестом создаём свежий экземпляр
counter = Counter();
});
test('value should start at 0', () {
expect(counter.value, 0);
});
test('value should be 1 after increment', () {
counter.increment();
expect(counter.value, 1);
});
test('value should be -1 after decrement', () {
counter.decrement();
expect(counter.value, -1);
});
});
}
Всё просто и понятно, да? Никакой магии.
А теперь пример посерьёзнее, где нужно замокать какую-нибудь зависимость, типа репозитория. Вот тут mocktail выходит на сцену.
import 'package:mocktail/mocktail.dart';
import 'package:test/test.dart';
// Допустим, у нас есть репозиторий, который ходит в сеть или базу
abstract class UserRepository {
Future<String> fetchUserName(int id);
}
// А это наш сервис, который мы и тестируем
class UserService {
final UserRepository repository;
UserService(this.repository);
Future<String> getUserName(int id) async {
return 'User: ${await repository.fetchUserName(id)}';
}
}
// А вот и наш мок! Создаётся одной строкой.
class MockUserRepository extends Mock implements UserRepository {}
void main() {
late UserService userService;
late MockUserRepository mockRepository;
setUp(() {
mockRepository = MockUserRepository();
userService = UserService(mockRepository);
});
test('getUserName should format data from repository', () async {
// Говорим моку, что возвращать, когда его вызовут
when(() => mockRepository.fetchUserName(1))
.thenAnswer((_) async => 'John');
// Запускаем наш сервис
final result = await userService.getUserName(1);
// Проверяем, что результат отформатирован как мы хотели
expect(result, 'User: John');
// И заодно проверяем, что метод репозитория вызвался ровно один раз с нужным аргументом
verify(() => mockRepository.fetchUserName(1)).called(1);
});
}
Красота же? Никаких лишних телодвижений. Подозрение ебать чувствую к тем, кто до сих пор пишет свои ручные заглушки на десять экранов.
А теперь главные правила, на которых я стою, как датский Гамлет на страже:
- Изоляция, ёпта! Каждый тест — это отдельная вселенная. Один тест не должен полагаться на побочные эффекты от другого, иначе потом будешь искать баг сутки, а он в соседнем тесте сидит. Используй
setUpиtearDownпо полной. - Одна штука — один тест. Не пытайся в одном тест-кейсе проверить, что метод и сохраняет данные, и отправляет уведомление, и чайник включает. Разбивай. Так и отлаживать проще, и когда что-то сломается, сразу понятно, где именно.
- Читаемость — всё. Если через месяц сам не сможешь понять, что этот тест делает, значит, ты сделал хуйню. Группируй тесты (
group), давай им внятные имена и не бойся выносить подготовку данных в отдельные функции. - Скорость, блядь! Unit-тесты должны бегать как угорелые. Если для их запуска нужен эмулятор или симулятор — ты делаешь что-то не то. Вся суть юнит-тестов в том, чтобы быстро давать обратную связь, прямо во время написания кода.