Запросы
Запросы включают в себя неявные входящие данные в программу. Относится к секции аргументов.
Рассмотрим простой пример — метод класса Date, используемый для получения текущей даты.
+new Date(); // 1741963243818
await wait(5_000);
// Функция получения текущей даты возвращает разный результат в зависимости от времени запуска
+new Date(); // 1741963320257
Обратите внимание, что функция получения текущей даты вернула разный результат, хотя аргументы, переданные в неё, не изменились.
Это ключевое свойство недетерминированных функций.
Детерминированность
Детерминированными называются функции, результат которых полностью определяется их входными аргументами:
// Функция double является детерминированной
const double = (n: number) => n * 2;
// Если вызвать её с тем же аргументом повторно
double(5); // 10
// Результат будет одинаковым
double(5); // 10
Источником недетерминированного поведения является скрытое изменяемое окружение — данные, влияющие на результат функции, но не передаваемые ей явно.
В дальнейшем, для простоты, будем называть его состоянием.
let counter = 0; // Внешняя переменная counter образует состояние incr
function incr() {
counter += 1;
return counter;
}
// incr не детерминированная
incr(); // 1
incr(); // 2
incr(); // 3
Для того чтобы сделать функцию incr детерминированной, необходимо избавиться от состояния:
function incr(initial = 0) {
// ^^^^^^^ аргумент initial не является скрытым состоянием
return {
value: initial + 1,
next: () => incr(initial + 1),
};
}
// incr теперь детерминированная функция
const iter_0 = incr();
iter_0.value // 1
// iter_0.next также является детерминированной и возвращает один и тот-же результат
const iter_1 = iter_0.next();
iter_1.value // 2
Данный пример иллюстрирует принцип моделирования состояния, а не рекомендуемый стиль реализации.
Изменяемое окружение
Наличие скрытого изменяемого окружения в AUT делает её поведение непредсказуемым и непригодным для эталонного тестирования.
Поэтому необходимо:
- Избавиться от внешнего состояния, поместив интерфейс доступа в слой аргументов.
- Подменяемый интерфейс должен быть максимально тонким и простым, для того чтобы сохранить защиту от регресса.
function formatDate(date: Date) {
// ^^^^ зависимость становится обратной
// ... //
}
function test() {
// Теперь, поведение функции легко контролируется
snapshot(formatDate(new Date(2026, 1, 1, 12, 0, 0, 0, 0)));
}
Принцип подстановки
Выделяя и подменяя запросы, тестовое окружение должно передавать такие детерминированные альтернативы, которые совместимы с ожидаемым программой интерфейсом:
declare function formatDate(date: Date);
// Тест валидный, так как передаётся объект с совместимым интерфейсом
snapshot(formatDate(new Date(2026, 1, 1, 12, 0, 0, 0, 0)));
// Тест невалидный, типы не совместимы
snapshot(formatDate(dayjs()));
Другими словами, подставляемое значение должно быть валидным подтипом ожидаемого.
В противном случае, тест будет проверять поведения невозможные в реальной среде исполнения.
Далее рассмотрим конкретные категории функций, зависящих от неявного изменяемого окружения.
Запросы к серверу
Сетевые запросы к серверу — самая простая категория недетерминированных функций:
// Результат getUserById напрямую зависит от текущего наполнения БД
getUserById(1); // { name: 'Vasiliy' }
// Спустя некоторое время...
getUserById(1); // 404
К компоненту "запросы" относятся только те сетевые запросы, что не изменяют наблюдаемых данных в БД (см. команды).
События среды
Некоторые события среды, также можно отнести к данной категории:
/**
* Подписывается на событие выхода компьютера из сна.
* Передаёт в обработчик общее время сна.
*/
declare function onComputerWakeUp(handle: (sleptForMS: number) => void);
Функционал, зависящий от onComputerWakeUp, будет сложно протестировать: мало того что она не детерминированная, так ещё и
зависит от трудно воспроизводимого окружения (помещения компьютера в сон).
Рассмотрим следующий пример:
// Показать уведомление
const notification = showMessage('Сообщение прочитано');
// Закрыть через 5 секунд
setTimeout(() => notification.close(), 5_000);
В рамках AUT setTimeout и настоящей спецификации можно рассматривать как детерминированную, однако, если оставить её
как есть, то она увеличит время выполнения тестов, что сильно повредит быстродействию.
Анимации
Начнём с того, что реализуем функцию таймер:
async function* onEachSecond(): AsyncGenerator<Date> {
while (true) {
await wait(1_000);
yield new Date();
}
}
Функция onEachSecond возвращает не один объект Date, а целую асинхронную последовательность.
На базе onEachSecond реализуем анимацию:
/**
* Анимации, будь то JS или CSS, всегда базируются на временном счётчике.
*/
const startedAt = new Date();
// onEachSecond() может контролировать скорость и направление анимации
for await (const now of onEachSecond()) {
const duration = sub(now, startedAt);
setPointPositionBy(point, duration);
}
Таким образом, анимации по своей природе также относятся к недетерминированному поведению.
В рамках тестирования AUT интерес представляет не бесконечный процесс анимации, а её дискретные наблюдаемые состояния.
Если оставить setTimeout, setInterval, анимации и другие подобные элементы без изменений, это не только увеличит
время выполнения тестов, но и усложнит их написание.
Связь с библиотекой
storyshots реализует следующие варианты подмены недетерминированного поведения:
- Анимации, мигающие курсоры и transitions подменяются библиотекой автоматически.
- Для JS анимаций следует использовать previewing флаг.
- Компоненты Web API, такие, как
setTimeout,Dateи другие, можно заменить инвазивным и неинвазивным способом. - Компонент "запросы" хранится в объекте
externalsи подменяется с помощью arrange