Irbisa · Шпаргалка

Тестирование

Юнит-тесты, тесты виджетов, интеграционные тесты, мокирование

Middle-разработчик · 25 вопросов

  1. 01

    Какие три вида тестов есть во Flutter и когда применять каждый?

    Unit-тесты проверяют одну функцию или класс, widget-тесты рендерят один виджет или экран без устройства, а интеграционные тесты гоняют всё приложение на эмуляторе или телефоне.

  2. 02

    Как устроен unit-тест в Dart: test, group, setUp/tearDown и expect?

    В тестовом файле есть main() с вызовами test('описание', body), сгруппированными через group; setUp выполняется перед каждым тестом своей группы, tearDown выполняется после каждого, а проверка в каждом тесте делается через expect(actual, matcher).

    import 'package:flutter_test/flutter_test.dart'; void main() {  group('Cart', () {    late Cart cart;…
  3. 03

    Как написать widget-тест: pumpWidget, finder-ы, tester.tap и матчеры?

    testWidgets даёт вам WidgetTester: строите виджет через pumpWidget, находите элементы через find, действуете через tap или enterText, вызываете pump для перестроения и проверяете через expect(finder, findsOneWidget).

    testWidgets('counter increments when + is tapped', (tester) async {  await tester.pumpWidget(const MaterialApp(home: CounterPage()));   expect(find.text('0'), findsOneWidget);  expect(find.text('1'), findsNothing);…
  4. 04

    Чем отличаются mock, stub и fake, и когда стоит написать fake руками вместо mocktail?

    Stub возвращает заготовленные ответы, mock ещё и записывает вызовы, чтобы их можно было проверить через verify, а fake служит маленькой рабочей реализацией, например репозиторием в памяти.

    class MockAnalytics extends Mock implements Analytics {} class InMemoryCartRepo implements CartRepo {  final _items = <Item>[];   @override…
  5. 05

    Этот тест на mocktail падает с type 'Null' is not a subtype of type 'Future<void>': почему и как исправить?

    class MockAnalytics extends Mock implements Analytics {} test('checkout logs a purchase', () async {  final analytics = MockAnalytics();  final cart = Cart(analytics);   await cart.checkout(); // calls analytics.logPurchase(42)   verify(() => analytics.logPurchase(42)).called(1);});

    logPurchase не застаблен, а незастабленный метод в mocktail возвращает null, который нельзя вернуть там, где объявлен non-nullable Future<void>.

  6. 06

    Найдите баг: этот тест на mocktail падает с No matching calls, хотя buyNow действительно отправляет Order('p1', 1).

    class Order {  Order(this.productId, this.qty);  final String productId;  final int qty;} class MockOrderApi extends Mock implements OrderApi {} test('Buy now sends one order for one item', () async {  registerFallbackValue(Order('', 0));  final api = MockOrderApi();  when(() => api.send(any())).thenAnswer((_) async {});  await Checkout(api).buyNow('p1'); // sends one Order   verify(() => api.send(Order('p1', 1))).called(1);});

    У Order нет равенства по значению, поэтому Order('p1', 1), созданный внутри verify, оказывается другим объектом, не тем, что отправил buyNow.

  7. 07

    Как тестировать виджет, который зависит от Theme, MediaQuery или Localizations?

    Нужно дать окружение, из которого виджет читает, и обычно это значит обернуть его в MaterialApp: если сверху ничего нет, MediaQuery.of(context) бросает исключение, а AppLocalizations.of(context)! падает на null.

    extension PumpApp on WidgetTester {  Future<void> pumpApp(Widget child, {Locale locale = const Locale('en')}) {    return pumpWidget(MaterialApp(      locale: locale,      localizationsDelegates: AppLocalizations.localizationsDelegates,      supportedLocales: AppLocalizations.supportedLocales,…
  8. 08

    Чем отличаются pump, pumpAndSettle, pumpFrames и runAsync в widget-тесте?

    Widget-тесты идут на фейковых часах: pump рисует один кадр и может сдвинуть время, pumpAndSettle качает кадры, пока ни один не запланирован, pumpFrames проходит фиксированный отрезок, а runAsync уходит на настоящие часы ради реального I/O.

    testWidgets('promo fades in over 300 ms', (tester) async {  await tester.pumpWidget(const MaterialApp(home: PromoPage()));  await tester.tap(find.text('Show'));  await tester.pump(); // starts the animation   FadeTransition fade() =>…
  9. 09

    Этот widget-тест падает с pumpAndSettle timed out: что происходит и как исправить?

    testWidgets('shows a spinner while the profile loads', (tester) async {  final pending = Completer<User>();  final repo = FakeRepo(onLoad: () => pending.future);   await tester.pumpWidget(MaterialApp(home: ProfilePage(repo: repo)));  await tester.pumpAndSettle();   expect(find.byType(CircularProgressIndicator), findsOneWidget);});

    Бесконечный (indeterminate) CircularProgressIndicator бесконечно планирует новый кадр, поэтому pumpAndSettle никогда не видит простоя и сдаётся после десяти минут фейкового времени.

  10. 10

    Widget-тест падает с Found 2 widgets with text "Save": как сузить finder, не хватаясь за .first?

    Ограничьте finder тем поддеревом или виджетом, который вы на самом деле имеете в виду.

    testWidgets('confirming the dialog saves the note', (tester) async {  await tester.pumpWidget(const MaterialApp(home: NoteEditor()));  await tester.tap(find.byTooltip('Edit'));  await tester.pumpAndSettle();   // wrong: the AppBar title and the dialog button both say 'Save'…
  11. 11

    Widget-тест тапает по 40-й строке ListView и падает с «nothing found», хотя в приложении экран работает: что происходит?

    ListView.builder строит только строки, которые помещаются в тестовую поверхность 800×600 плюс небольшой кэш, поэтому строки 40 в дереве нет и ни один finder её не увидит.

    testWidgets('tapping a far-down order opens it', (tester) async {  await tester.pumpWidget(const MaterialApp(home: OrdersPage()));   // wrong: row 40 was never built, so the finder matches nothing  // await tester.tap(find.text('Order 40'));…
  12. 12

    Как в widget-тесте доказать, что тап по кнопке открыл нужный экран?

    Проверьте, что после тапа на экране целевая страница: прокачайте MaterialApp с маршрутами, тапните, вызовите pumpAndSettle и сделайте expect(find.byType(CheckoutPage), findsOneWidget).

    testWidgets('cart button opens checkout', (tester) async {  await tester.pumpWidget(MaterialApp(    home: const CartPage(),    routes: {'/checkout': (_) => const CheckoutPage()},  ));…
  13. 13

    Как тестировать асинхронный код и стримы: await, expectLater и emitsInOrder?

    Делайте тело теста async и просто ждите future через await; для стрима навесьте expectLater(stream, emitsInOrder([...])) до того, как запустить работу, а потом дождитесь его.

    test('emits loading, then the profile', () async {  final cubit = ProfileCubit(FakeProfileRepo());  addTearDown(cubit.close);   final states = expectLater(    cubit.stream,…
  14. 14

    Как протестировать debounce, retry с backoff или поллинг, не замедляя тесты?

    Не вставляйте в тесты реальные паузы.

    test('debounce runs only the last call after 300 ms of quiet', () {  fakeAsync((async) {    final calls = <String>[];    final debouncer = Debouncer(const Duration(milliseconds: 300));     debouncer.run(() => calls.add('f'));…
  15. 15

    Как тестировать код с HTTP-запросами, не обращаясь к реальной сети?

    Для widget- и bloc-тестов подменяйте репозиторий fake-ом, а сам HTTP-клиент подменяйте, только когда тестируемый код и есть HTTP-слой.

    import 'package:http/http.dart' as http;import 'package:http/testing.dart'; test('UserApi parses the user and sends the token', () async {  late http.Request sent;  final client = MockClient((request) async {…
  16. 16

    Как написать widget-тест для экрана, состояние которого живёт в Riverpod или BLoC?

    Подменяйте то, от чего зависит слой состояния, а не виджеты: переопределите провайдеры в ProviderScope или отдайте экрану замоканный блок через BlocProvider.value, а затем проверяйте отрисованное.

    testWidgets('Riverpod: renders the profile from a fake repo', (tester) async {  await tester.pumpWidget(ProviderScope(    overrides: [profileRepoProvider.overrideWithValue(FakeProfileRepo())],    child: const MaterialApp(home: ProfilePage()),  ));  await tester.pump(); // let the FutureProvider resolve…
  17. 17

    Как писать golden-тесты во Flutter и как сделать их стабильными и убрать из быстрого прогона?

    Golden-тест рендерит виджет, сравнивает пиксели с закоммиченным PNG через matchesGoldenFile и падает при любом расхождении; flutter test --update-goldens перегенерирует картинку после намеренного изменения.

    @Tags(['golden'])library; import 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';…
  18. 18

    Как писать интеграционные тесты на integration_test и когда нужен patrol?

    integration_test запускает обычный код testWidgets против настоящего приложения на эмуляторе или устройстве, с настоящими плагинами и движком; patrol строится поверх него и умеет управлять ещё и нативным UI вроде диалогов разрешений и уведомлений.

    // integration_test/checkout_test.dartimport 'package:flutter/material.dart';import 'package:flutter_test/flutter_test.dart';import 'package:integration_test/integration_test.dart';import 'package:shop/main_test.dart' as app; // entry point with a fake backend…
  19. 19

    Как измерить покрытие кода во Flutter-проекте и как его стоит использовать в CI?

    flutter test --coverage пишет coverage/lcov.info; уберите из него сгенерированные файлы, а в CI ставьте порог на покрытие изменённых строк, а не на фиксированный общий процент.

    flutter test --coverage # drop generated code before measuringlcov --remove coverage/lcov.info \  '*.g.dart' '*.freezed.dart' '*/l10n/*' \  -o coverage/lcov.info…
  20. 20

    Вы отрефакторили внутренности репозитория, не меняя поведения, и покраснели тридцать тестов: что эти тесты делали не так?

    Они проверяли, как код работает, а не что он делает, поэтому рефакторинг (то самое, что тесты должны делать безопасным) их сломал.

    // wrong: pins the call and the tree shape, breaks on any refactortestWidgets('loads the cart', (tester) async {  final repo = MockCartRepo();  when(() => repo.fetchPage(1)).thenAnswer((_) async => [apple]);  await tester.pumpWidget(MaterialApp(home: CartPage(repo: repo)));  await tester.pumpAndSettle();…
  21. 21

    Практикуете ли вы TDD и как написание тестов первыми меняет то, как вы проектируете классы?

    Red, green, refactor: пишете падающий тест на следующее поведение, делаете так, чтобы он прошёл, самым простым кодом, затем чистите.

  22. 22

    Ваш виджет бросает исключение в build на плохих данных, и тест умирает с этим исключением: как вместо этого проверить его?

    Заберите его через tester.takeException().

    testWidgets('a malformed price surfaces as a FormatException', (tester) async {  await tester.pumpWidget(const MaterialApp(home: PriceTag(raw: 'n/a')));   // the framework already caught it; claim it or the test fails at the end  expect(tester.takeException(), isA<FormatException>());});…
  23. 23

    Набор тестов зелёный на вашей машине, а в CI каждый прогон падает один случайный тест: как найти причину?

    Подозревайте утёкшее общее состояние раньше, чем тайминги: тесты внутри одного файла делят глобальные переменные, поэтому тест, который проходит один и падает после другого, читает то, что оставил предыдущий.

  24. 24

    Найдите баг: этот тест с fakeAsync остаётся зелёным, даже если код истечения сессии сломан.

    test('session expires after 15 minutes of inactivity', () {  fakeAsync((async) async {    final session = Session(FakeAuthApi(), timeout: const Duration(minutes: 15));    await session.start(); // signs in, then starts the inactivity timer     async.elapse(const Duration(minutes: 15));    expect(session.isExpired, isTrue);  });});

    Колбэк fakeAsync помечен async, поэтому он останавливается на await session.start() и возвращает future, которую никто не ждёт.

  25. 25

    Как бы вы спроектировали стратегию тестирования большого Flutter-приложения: что идёт в unit-, widget-, golden- и интеграционные тесты?

    Переносите большую часть проверок вниз, в быстрые unit-тесты доменного слоя и слоя состояния, покройте все состояния каждого экрана widget-тестами против fake, защитите дизайн-систему goldens и держите небольшой набор интеграционных тестов для сценариев, которые приносят деньги.