Перейти к содержимому

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

Тест виджета запускает загрузку и смотрит на экран, когда она закончилась. Контроллер и экран — это ProfileController и ProfileView из раздела «Как пользоваться» README пакета. FakeApi принадлежит тесту: он отвечает именем через десять миллисекунд после вызова.

testWidgets работает на фейковых часах, и отсюда три правила:

  • часы двигает тест, через pump или pumpAndSettle; ожидание Job их не двигает;
  • тест, который читает done, читает его до того, как сдвинет часы;
  • тест закрывает контроллер в своём теле, а не в addTearDown.

Обычный тест дожидается Job; о таком тесте рассказывает «Тестирование» solo.

// Первая попытка: тест ждёт задачу, а задача ждёт тест.
await controller.load().done;
await tester.pump();

Дальше первой строки тест не идёт:

  1. Фейк отвечает через десять миллисекунд, и эти миллисекунды — таймер фейковых часов.
  2. Внутри testWidgets эти часы двигает только сам тест.
  3. Задача ждёт, пока тест сдвинет время, а тест ждёт задачу.
  4. Тест стоит так до своего таймаута, по умолчанию десять минут.
testWidgets('the profile appears', (tester) async {
final controller = ProfileController(FakeApi());
await tester.pumpWidget(
MaterialApp(home: ProfileView(controller: controller)),
);
await tester.tap(find.text('Load'));
await tester.pumpAndSettle();
expect(find.text('Ada Lovelace'), findsOneWidget);
await controller.close();
});

pumpAndSettle прокачивает по кадру каждые сто миллисекунд, пока кто-то просит следующий кадр. Первой сотни этому фейку хватает с запасом. Дождались бы и более медленного, потому что спиннер состояния Loading всё время просит кадры.

Ждут здесь экран, а не задачу. Когда ничего не анимируется, pumpAndSettle останавливается через кадр или два, а загрузка, которой нужно больше, так и ждёт.

Контроллер закрывается в теле теста, а не в addTearDown. Колбэк addTearDown выполняется, когда фейковые часы уже остановились. close(), которого там ждут при ещё идущей задаче, не возвращается никогда, а оставить задачу идущей может и проверка, упавшая посреди загрузки. Тест стоит до своего таймаута, и следующий за ним тест падает вместе с ним.

Таймер, который ещё ждёт, когда кончается тело testWidgets, роняет тест с A Timer is still pending even after the widget tree was disposed, и у идущей загрузки такой таймер есть: десять миллисекунд фейка. Job, которой дали timeout, приносит ещё один, и ядро держит его, пока Job не кончится. Контроллер, закрытый в теле, как выше, кончает Job и забирает её срок с собой, но не вызов, от которого Job ушла: фейк всё равно ответит через десять миллисекунд после вызова, и тест кончается чисто, когда pump с длительностью переведёт часы дальше этого ответа.

Хэндл ждёт саму задачу:

final done = controller.load().done; // до того, как Job может кончиться
await tester.pump(const Duration(milliseconds: 20)); // двигаем часы
final outcome = await done;

pump с длительностью сдвигает часы, а потом рисует, поэтому оставленный им кадр показывает последнее состояние.

done читается в первой строке, до того как часы сдвинулись, и порядок важен. За Failed, про который никто не спросил, движок отвечает на микрозадаче после конца Job, и без Solo.unansweredHandler отправляет его в зону, где Job создали. Здесь это зона теста, и она тут же проваливает тест, а tester.takeException() после этого забирать нечего. Провал, который назвал isExpectedError, туда не идёт: загрузка, упавшая с ApiException, оставляет тест зелёным, когда бы он ни читал done. Загрузка, упавшая из-за бага, делает тест красным, хотя он и читает done, если читает его после прокачки.

Job, которую тест запускает и бросает, больше ничего не требует, когда её контроллер называет свои ожидаемые ошибки, как это делает load. Брошенная Job без такого предиката говорит об этом через ignoreFailure().

pump() без длительности решает, рисовать ли, раньше, чем выполняет микротаску, с которой стартует Job. Поэтому после controller.load() и одного pump() состояние сдвинулось, а экран нет, если только кадр не был запрошен заранее, как сразу после прокачки MaterialApp. Рисует его второй pump().