Тестирование
Тест виджета запускает загрузку и смотрит на экран, когда она закончилась.
Контроллер и экран — это ProfileController и ProfileView из раздела
«Как пользоваться»
README пакета. FakeApi принадлежит тесту: он отвечает именем через десять
миллисекунд после вызова.
testWidgets работает на фейковых часах, и отсюда три правила:
- часы двигает тест, через
pumpилиpumpAndSettle; ожиданиеJobих не двигает; - тест, который читает
done, читает его до того, как сдвинет часы; - тест закрывает контроллер в своём теле, а не в
addTearDown.
Обычный тест дожидается Job; о таком тесте рассказывает
«Тестирование» solo.
Первая попытка
Заголовок раздела «Первая попытка»// Первая попытка: тест ждёт задачу, а задача ждёт тест.await controller.load().done;await tester.pump();Дальше первой строки тест не идёт:
- Фейк отвечает через десять миллисекунд, и эти миллисекунды — таймер фейковых часов.
- Внутри
testWidgetsэти часы двигает только сам тест. - Задача ждёт, пока тест сдвинет время, а тест ждёт задачу.
- Тест стоит так до своего таймаута, по умолчанию десять минут.
Двигаем часы
Заголовок раздела «Двигаем часы»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().