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

Команды

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

Команды включают неявные исходящие данные программы — результаты её работы, наблюдаемые за пределами AUT. Относится к секции результата.

Сайд-эффект

Сайд-эффект — наблюдаемый клиентами результат работы функции, который существует за пределами её возвращаемого значения.

примечание

В контексте данной спецификации, говоря о "функции", имеется ввиду AUT, то есть целая программа, которую необходимо сделать тестируемой.

Соответственно, сайд-эффект — это результат работы AUT, выходящий за пределы самой тестируемой программы.

throw new Error() сайд-эффект?

Рассмотрим простую функцию деления:

function divide(a: number, b: number): number {
if (b === 0) {
throw new Error('Cannot divide by zero');
}

return a / b;
}
  • Нельзя сказать, что выброшенное исключение является возвращаемым результатом работы функции: его нельзя записать в переменную и оно требует специальной обработки.
  • К тому же, глядя исключительно на сигнатуру function divide(a: number, b: number): number невозможно понять, какие исключения выбрасываются и выбрасываются ли вообще.

Теперь взглянем на альтернативную версию:

function divide(a: number, b: number): Either<number, CanNotDivideByZero> {
if (b === 0) {
return left(new CanNotDivideByZero())
}

return right(a / b);
}
  • Теперь ошибка явно возвращается из функции как значение.
  • Сигнатура делает функцию прозрачной, сообщая о возможном исключении.

На таком контрасте видно, что исключения являются сайд-эффектами, однако они не выходят за пределы AUT и потому не рассматриваются как команды в контексте тестирования.

Внешняя среда

Проще всего понять определение на следующем примере:

declare function getUsers(): Promise<User[]>;
declare function addUser(body: UserBody): Promise<User[]>;

/**
* Функция getUsers возвращает список пользователей из БД.
*
* При первом запуске список пуст, так как пользователи ещё не добавлены.
*/
await getUsers(); // []

/**
* Функция addUser добавляет нового пользователя в БД, ничего при этом не возвращая.
*/
await addUser({ name: 'Vasiliy' });

/**
* При повторном вызове getUsers результат изменится.
*
* Функция addUser изменила внешнее состояние, которое повлияло на результат getUsers.
*/
await getUsers(); // [{ id: 1, name: 'Vasiliy' }]
  • addUser вернул void в качестве результата.
  • Но, при этом addUser осуществила действие, которое повлияло на работу стороннего клиента — функцию getUsers.

Помимо простых команд, существуют функции которые одновременно осуществляют сайд-эффект и возвращают результат:

// Сканирует штрих-код с камеры устройства
const code = await scanBarCodeWithCamera();

// Запрашивает разрешения у пользователя
const granted = askForPermissions(permisions);

// Активировать вибрацию на телефоне, выбрасывает исключение при неудаче
vibratePhone();

// Показать системное окно подтверждение
const confirmed = window.confirm('Are you sure?');

Все перечисленные функции объединяет одно очень важное свойство — все они выполняют сайд-эффекты во внешнюю среду.

примечание

Внешняя среда — это данные, живущие за пределами AUT. Сайд-эффект изменяет эти данные, что в свою очередь может сделать поведение AUT недетерминированным.

Способ верификации

Согласно установленным условиям верификации, необходимо:

  • Выразить сайд-эффекты в виде значений, для того чтобы их можно было записать в эталон.
  • Не выполнять сайд-эффекты AUT в тестовом окружении в целях управляемости.

Для этого используется комбинация подмены и журналирования.

При стандартном подходе реализация команды лишается сайд-эффектов и факт вызова записывается в журнал:

function showNativeNotification(config: NotificationConfig) {
/*
Важно записывать полную информацию о функции, когда и с какими параметрами она вызывается,
так как всё это является частью контракта взаимодействия.
*/
journal.record('showNativeNotification', config);
// Ничего не выполняем в тестах
}

/**
* В начале журнал вызовов пуст — []
*/

// ...

/**
* После первого вызова в журнале появится запись с информацией о вызове и переданных аргументах.
* [["showNativeNotification", [{ title: 'Hello, User!' }]]]
*/
await showNativeNotification({ title: 'Hello, User!' });

/**
* Журнал будет пополняться при вызове подобных функций далее.
*/

Во время тестов программа вызывает методы и заполняет журнал. В конце работы журнал сравнивается с эталоном, чтобы проверить корректность работы приложения.

Схема процесса:

Журнал становится слепком сайд-эффектов AUT. Данный артефакт может использоваться разработчиком для валидации контракта взаимодействия с внешним миром.

примечание

При такой модели, если наблюдаемое поведение нарушилось, но при этом эталон журнала не изменился, это может означать следующее:

  • Нарушилось поведение внешних систем
  • Изменились контракты взаимодействия

В обоих случаях, регресс порождён внешними изменениями, а не повреждением внутренней реализации.

Сигналы

Можно выделить отдельную категорию команд, которые хоть и выполняют сайд-эффект, однако его результат никак не влияет на работу AUT:

// Функция отправляет чек по email
const success = sendReceiptByEmail(receipt);

Всё что обрабатывается AUT — факт успешности выполнения сайд-эффекта, но не его результат. Можно сказать, что такие функции просто посылают сигналы во внешние системы (fire and forget).

Тем не менее, в целях тестирования также стоит выделять такие функции в команды, по следующим причинам:

  • Журнал верифицирует контракт взаимодействия. Сигналы должны быть совместимы с интерфейсом внешних систем.
  • Сигналы не должны уходить во внешние системы во время тестирования, в целях управляемости.

Эмуляция

Для команд также существует иной метод верификации, а именно — эмуляция:

// В тесте поведение getUsers и addUser эмулируется
const users: User[] = [];

async function getUsers(): Promise<User[]> {
return users;
}

async function addUser(body: UserBody): Promise<User[]> {
users.push(createUserFromBody(body));
}

async function test() {
// Отображается список с пустым набором пользователей
snapshot(renderUI(await getUsers())); // []

await addUser({ name: 'Vasiliy' });

// Отображается список с добавленным пользователем
snapshot(renderUI(await getUsers())); // [{ id: 1, name: 'Vasiliy' }]
}
примечание

addUser по-прежнему осуществляет сайд-эффект по отношению getUsers, однако, он не выходит за пределы AUT. Программа остаётся детерминируемой, а значит — тестируемой.

Данный метод позволяет обеспечить большую защиту от регресса, так как поведение внешней системы теперь эмулируется, что приближает условия работы AUT к целевым.

Но, взамен сам тестовый сценарий усложняется, в нём появляется дополнительная логика.

подсказка

Если поведение внешней системы тривиальное, а AUT — сложное, то отдавайте предпочтение эмуляции. В противном случае используйте журналы.

Связь с библиотекой

storyshots позволяет тестировать функции, подобные addUser, двумя способами:

  • С использованием журнала вызовов (рекомендуется для большинства случаев).
  • С помощью эмуляции поведения (усложняет тесты, но повышает защиту от регресса).
  • Компонент "команды" хранится в объекте externals и подменяется с помощью arrange