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

Запросы

📈 Улучшает
🛡🔧 Ухудшает

Запросы включают в себя неявные входящие данные в программу. Относится к секции аргументов.

Рассмотрим простой пример — метод класса 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
warning

Данный пример иллюстрирует принцип моделирования состояния, а не рекомендуемый стиль реализации.

Изменяемое окружение

Наличие скрытого изменяемого окружения в 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