Команды
Команды включают неявные исходящие данные программы — результаты её работы, наблюдаемые за пределами 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