Что вы использовали для написания unit-тестов во Flutter?

Ответ

В моих 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);
  });
}

Ключевые принципы, которых я придерживаюсь:

  1. Изоляция: Каждый тест должен быть независимым и не полагаться на состояние, созданное другими тестами.
  2. Одна ответственность: Тест проверяет одну конкретную функциональность или сценарий.
  3. Читаемость: Использую setUp/tearDown для подготовки и очистки, а group для логической группировки тестов.
  4. Скорость: 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);
  });
}

Красота же? Никаких лишних телодвижений. Подозрение ебать чувствую к тем, кто до сих пор пишет свои ручные заглушки на десять экранов.

А теперь главные правила, на которых я стою, как датский Гамлет на страже:

  1. Изоляция, ёпта! Каждый тест — это отдельная вселенная. Один тест не должен полагаться на побочные эффекты от другого, иначе потом будешь искать баг сутки, а он в соседнем тесте сидит. Используй setUp и tearDown по полной.
  2. Одна штука — один тест. Не пытайся в одном тест-кейсе проверить, что метод и сохраняет данные, и отправляет уведомление, и чайник включает. Разбивай. Так и отлаживать проще, и когда что-то сломается, сразу понятно, где именно.
  3. Читаемость — всё. Если через месяц сам не сможешь понять, что этот тест делает, значит, ты сделал хуйню. Группируй тесты (group), давай им внятные имена и не бойся выносить подготовку данных в отдельные функции.
  4. Скорость, блядь! Unit-тесты должны бегать как угорелые. Если для их запуска нужен эмулятор или симулятор — ты делаешь что-то не то. Вся суть юнит-тестов в том, чтобы быстро давать обратную связь, прямо во время написания кода.