Тестирование
Задача идёт на таймерах и микротасках, поэтому тест может прогнать её
во времени под своим управлением и ничего не ждать в реальном времени. Такое
время тесту даёт package:fake_async.
Отменённое открытие
Заголовок раздела «Отменённое открытие»Тест проверяет, что при отмене во время открытия база всё равно закрывается,
когда открытие завершится. discard закрывает значение, которое пришло после
отмены, как в разделе
«Значение, которое приходит поздно».
Первая попытка
Заголовок раздела «Первая попытка»cancel() возвращает future, которая завершается, когда задача кончилась,
поэтому тест её ждёт:
// Первая попытка: ни один expect не выполняется.test('a cancelled open still closes what it opened', () { fakeAsync((async) async { var closed = 0; final job = Job<Database>( (ctx) => ctx.join( Database.open, discard: (db) { closed++; return db.close(); }, ), );
async.elapse(const Duration(milliseconds: 10)); await job.cancel();
expect(job.outcome, isA<Cancelled>()); expect(closed, 1); });});Тест проходит, и с expect(closed, 0) прошёл бы точно так же: ни один expect
не выполняется. Вот что происходит:
- Под
fakeAsyncвремя движется, только когда его двигает тест. - Колбэк возвращается на первом
await, иfakeAsyncвозвращается вместе с ним. - Время больше никто не двигает, так что future из
cancel()не завершается никогда, и код после неё не выполняется.
Если написать () => fakeAsync(...), тест будет ждать эту future и упадёт
по таймауту.
Тест двигает время
Заголовок раздела «Тест двигает время»test('a cancelled open still closes what it opened', () { fakeAsync((async) { var closed = 0; final job = Job<Database>( (ctx) => ctx.join( Database.open, discard: (db) { closed++; return db.close(); }, ), );
async.elapse(const Duration(milliseconds: 10)); job.cancel().ignore(); // внутри `fakeAsync` никто ничего не ждёт async.flushTimers();
expect(job.outcome, isA<Cancelled>()); expect(closed, 1); // ради чего тест и назван });});Внутри fakeAsync никто ничего не ждёт. Future из cancel() проигнорирована,
flushTimers() доводит открытие до конца, и проверки видят то, что произошло.
Чем двигать время, зависит от того, чего тест ждёт:
| Тест ждёт | Он вызывает |
|---|---|
| Запуска задачи, чьё тело начинается в микротаске | flushMicrotasks() |
Работы, запланированной через Future(...) или Future.delayed(...), которые используют таймеры |
flushTimers() или elapse(...) |
Паузы ctx.pause(duration), которая ждёт на таймере |
elapse(duration) или flushTimers() |
Срока из timeout: limit, который ядро держит на таймере |
elapse(limit) или flushTimers() |
Настоящий ввод-вывод под fakeAsync не завершается. Его колбэк не отдаёт ход
настоящему циклу событий, поэтому чтение файла или запрос к базе, которых ждёт
задача, висят, сколько бы тест ни двигал время. Такой тест подделывает только
таймеры: он идёт в своей зоне, чья ZoneSpecification переопределяет один
createTimer, и между шагами даёт настоящему циклу событий провернуться. Время
ядро берёт только из таймеров, поэтому ctx.pause и timeout: задачи,
созданной в этой зоне, идут по поддельным часам, и задачу менять не нужно.
Провал, которого тест ждёт
Заголовок раздела «Провал, которого тест ждёт»Тест проверяет, что открытие запертой базы падает.
Первая попытка
Заголовок раздела «Первая попытка»// Первая попытка: проверка проходит, а тест падает.test('a locked database fails the open', () { fakeAsync((async) { final job = Job<Database>((ctx) => ctx.join(Database.open)); async.flushTimers();
expect(job.outcome, isA<Failed>()); });});Bad state: database lockedexpect проходит, а тест падает с той самой ошибкой, которую проверяет. Чтение
outcome провал не наблюдает:
никто не прочитал done или value и не вызвал ignoreFailure(), поэтому
провал уходит в зону, где задачу создали. Это
зона теста, а package:test роняет тест, до зоны которого дошла ошибка.
Ошибки, которых не несёт ни один исход, например ошибки уборки или
ctx.unattended, попадают туда же, если за них
не отвечает наблюдатель: см.
«Куда идёт каждая ошибка».
Провал читает тест
Заголовок раздела «Провал читает тест»test('a locked database fails the open', () { fakeAsync((async) { final job = Job<Database>((ctx) => ctx.join(Database.open)) ..ignoreFailure(); // провал тест читает из исхода async.flushTimers();
expect(job.outcome, isA<Failed>()); });});ignoreFailure() говорит, что о провале кто-то позаботится, и здесь это тест.
Тест, который читает job.done или job.value, тоже его наблюдает.
Сравнение исходов
Заголовок раздела «Сравнение исходов»Тест проверяет значение задачи через её исход.
Первая попытка
Заголовок раздела «Первая попытка»// Первая попытка: два исхода, которые печатаются одинаково, не равны.test('the job loads three', () { fakeAsync((async) { final job = Job<int>((ctx) => ctx.join(load)); async.flushTimers();
expect(job.outcome, const Done(3)); });});Expected: Done<int>:<Done(3)> Actual: Done<int>:<Done(3)>Своего == у исхода нет, поэтому два исхода равны, только когда это один
объект, а исход задачи никогда не тот, который строит тест.
По типу и значению
Заголовок раздела «По типу и значению»test('the job loads three', () { fakeAsync((async) { final job = Job<int>((ctx) => ctx.join(load)); async.flushTimers();
expect( job.outcome, isA<Done<int>>().having((done) => done.value, 'value', 3), ); });});С остальными так же: isA<Cancelled>() с having по reason
и isA<Failed>() с having по error.
Настройки между тестами
Заголовок раздела «Настройки между тестами»setUp(() { final previous = Job.isCancellationError; Job.isCancellationError = (error) => error is DatabaseStopped; addTearDown(() => Job.isCancellationError = previous);});Job.isCancellationError и Job.debug статические. Тест, который задаёт одно
из них, задаёт его и для каждого теста после себя в том же файле: оставленный
фильтр не пускает названные им ошибки в зону этих тестов. addTearDown
возвращает то, что было.