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

Написание заглушек

storyshots благодаря своей архитектуре сводит к минимуму действия требуемые при написании тестовых сценариев, однако, остаётся одна область которая будет требовать существенного внимания от разработчика, а именно - заглушки.

примечание

Заглушки занимают наибольший суммарный объём кода в тестах написанных с использованием storyshots.

Заглушки фабрики

📈 Улучшает

Заглушки можно описать двумя основными способами:

  • В виде статичного объекта:
const userStub = {
name: 'Vasiliy',
position: {
role: {
isAdmin: true,
},
},
};
  • И в виде фабрики:
function createUserStub(): User {
return {
name: 'Vasiliy',
position: {
role: {
isAdmin: true,
},
},
};
}

В большинстве случаев, рекомендуется использовать фабрики. Это объясняется следующими причинами:

Меньше времени на инициализацию

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

примечание

Создаваться будут все стабы без исключения, даже если они не используются в выполняемом тесте.

Фабрики лишены данной проблемы, так как они представляют собой ленивые данные, которые будут созданы только тогда, когда они понадобятся в истории.

Упрощение изменений

Статичные объекты, а тем более константы опасно мутировать напрямую, ведь они могут повлиять на поведение других своих клиентов:

it('...', {
arrange: (user) => ({
asNonAdmin: async () => {
/**
* Объект опасно изменять напрямую, ведь он может использоваться где то ещё.
*/
userStub.position.role.isAdmin = false;

return userStub;
},
/**
* Другая функция ссылается на тот же объект и будет затронута поведением `asNonAdmin`,
* что может быть нежелательным.
*/
getAdmin: () => userStub,
}),
});

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

it('displays living address', {
arrange: (user) => ({
// Теперь эффект изолирован в функции `asFired`
asNonAdmin: async () => ({
...userStub,
authorities: {
...userStub.position,
role: {
...userStub.position.role,
isAdmin: false,
},
},
}),
getAdmin: () => userStub,
}),
});

Это нагружает синтаксис и усложняет чтение теста. С фабриками всё проще:

it('displays living address', {
arrange: (user) => ({
asFired: async () => {
const user = createUserStub();
/**
* Объект можно спокойно изменять напрямую, так как мы работаем с уникальной копией user.
*/
user.position.role.isAdmin = false;

return user;
},
/**
* Другая функция ссылается на свой экземпляр user.
*/
getAdmin: () => createUserStub(),
}),
});
примечание

Такое свойство фабрик (их чистота), делает их простыми с точки зрения расширяемости и повторного использования. Поэтому в данной документации все основные примеры демонстрируются с использованием преимущественно данного подхода.

подсказка

Использование статичных переменных и их прямое изменение допускается (и даже приветствуется) в целях моделирования совместимых поведений.

Модульные заглушки

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

Одним из вариантов их упрощения является использование модульной структуры схожей с модульной внешней средой. Если расширяемой единицей в случае репозиториев был метод, то в случае с заглушками это будут её свойства:

Вместо этого:

function createUserStub(): User {
return {
name: 'Vasiliy',
position: 'developer',
living: {
city: 'Moscow',
},
/* И ещё 20+ свойств */
};
}

Делать это:

/**
* Описать минимальный набор полей для всех историй или оставить пустым
*/
function createMinimalUserStub(): User {
return {
name: 'Vasiliy',
position: 'developer',
} as User;
}

it('displays living address', {
arrange: (user) => ({
getUser: async () => ({
...createMinimalUserStub(),
living: { city: 'Moscow' }, // История устанавливает только нужные ей поля
}),
}),
});

Модульность позволяет историям определять только нужные им данные в заглушках, но это лишь один из инструментов сокращения их объёма.

примечание

Заглушки также наследуют от репозиториев проблемы связанные с отвественностью, которые были описаны подробно в разделе модульная внешняя среда.

Важно

Следует применять с осторожностью решения для генерации заглушек на подобии faker-js. Так как они зачастую производят недереминированные данные.

Неизбыточные заглушки

📈 Улучшает

Следует определять только те методы, что на самом деле используются приложением.

примечание

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

Довольно частым виновником больших по объёму стабов является не само поведение, а типы описанные в исходном коде:

// Все поля в модели обязательные и при этом их достаточно много
type User = {
name: string;
position: string;
living: {
city: string;
};
/* И ещё 20+ свойств */
};
примечание

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

Проблема заключается в том, что данные типы просто копируются из сторонних систем, например swagger и в конечном итоге не отражают реального положения вещей: приложение может использовать только 5 полей из 20, в то время как в заглушках объявляются все свойства без исключений.

Вместо этого:

type User = {
name: string;
position: string;
living: {
city: string;
};
};

Делать это:

/**
* Делать модель более точной и избавляться от неиспользуемых полей
*/
type User = {
name: string;
position?: string;
};
Важно

На проектах зачастую используются разного рода генераторы интерфейсов API. Это удобный инструмент, однако итоговая точность моделей может быть недостаточной.

Релевантные заглушки

🛡📈 Улучшает

Тестовые данные (или другими словами стабы), являются критически важным элементом тестового сценария. Важно, чтобы содержание заглушек соответствовало доменным требованиям и условиям диктуемым моделью:

Вместо этого:

function createUserStub(): User {
return {
name: 'UserName',
roles: [],
};
}

Делать это:

function createUserStub(): User {
return {
name: 'Васильев Василий Васильевич', // Имя пользователя содержит ФИО
roles: ['admin'], // Роли по модели не могут быть пустыми
};
}

Релевантные по отношению к предметной области и модели заглушки приносят следующие преимущества в тесты:

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

Определение нерелевантных заглушек можно сравнить с тем что мы заставляем наше приложение общаться со "сломанным" сервером, что очевидно негативно повлияет на стабильность самих историй и увеличит их хрупкость.

Достаточные заглушки

📈 Улучшает

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

Вместо этого:

function createUserStub(): User {
return {
name: 'Vasiliy',
address: 'Moscow',
};
}

Делать это:

function createUserStub(): User {
return {
name: 'Vasiliy',
/**
* Поле с адресом убрано так как оно не используется в приложении
* и как следствие не требуется для покрытия всех его сценариев.
*/
};
}
подсказка

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

Упрощённые заглушки

📈 Улучшает

Тестовые данные содержат в себе множество комплексных полей, такие как: идентификаторы, ссылки на другие сущности, даты. Следует устанавливать как можно более простые значения для таких свойств.

подсказка

Это снижает сложность самих заглушек и при этом никак не влияет на покрытие тестов.

Вместо этого:

function createUserStub(): User {
return {
id: 5142,
name: 'Vasiliy',
updatedAt: '2024-06-28T15:00:00.000Z',
};
}

Делать это:

function createUserStub(): User {
return {
id: 1,
name: 'Vasiliy',
updatedAt: createConstDate().toJSON(),
};
}

/**
* Установить любую фиксированную дату, актуальную для всех историй. По необходимости её можно смещать в конкретных фабриках.
*/
declare function createConstDate(): Date;