Истории
История это базовый элемент storyshots. Она фиксирует приложение в определённом состоянии, описывая параметры
внешнего окружения и действия пользователя.
Разделение историй
storyshots реализует множество инструментов направленных на разделение тестовых сценариев. Один из них - это
использование семантических групп describe.
Большое кол-во сценариев, объединённых вместе не только увеличивают размер файла, но и усложняют свою поддержку ввиду разности их ответственностей.
Ответственность - это причина по которой код может быть изменён. Если две разные функции меняются в одно и то же время и по одной и той же причине, то тогда, и только тогда их ответственности считаются равными.
Вместо этого:
const stories = [
it('shows list of products' /* ... */),
it('allows to add a product' /* ... */),
it('rejects unauthorized access to a store' /* ... */),
];
Делать это:
const stories = [
describe('Products', [
it('shows list of products' /* ... */),
it('allows to add a product' /* ... */),
]),
describe('Auth', [it('rejects unauthorized access to a store' /* ... */)]),
];
Истории должны быть декомпозированы таким образом, чтобы при их последующем редактировании (или чтении) в них было как можно меньше избыточных элементов.
describeблоки - это чаще всего наименование домена, подфункции или отвественности. Рекомендуется именовать коротким словосочетанием в CamelCase (с заглавной буквы).itблоки - это конкретная история, читается как - "Это приложение (it) текст истории".
it('allows for user to logout');
Читается как - "Это приложение позволяет пользователью выйти из учётной записи".
Слияние историй
storyshots позволяет в рамках одной истории сделать не один снимок экрана, а сразу несколько:
Вместо этого:
describe('Login', [
it('shows disabled password initially'), // Тест просто делает снимок изначального состояния формы
it('allows to fill login field', {
// Проверяет возможность заполнения поля
act: (actor) => actor.fill(finder.getByPlaceholder('Login'), 'Логин'),
}),
it('allows to enter credentials', {
// Проверяет возможность заполнения всей формы
act: (actor) =>
actor
.fill(finder.getByPlaceholder('Login'), 'Логин')
.fill(finder.getByPlaceholder('Password'), '1235'),
}),
]);
Делать это:
it('allows to enter credentials', {
// История проверяет все состояния сразу
act: (actor) =>
actor
.screenshot('Initial')
.fill(finder.getByPlaceholder('Login'), 'Логин')
.screenshot('PasswordEnabled')
.fill(finder.getByPlaceholder('Password'), '1235'),
});
Изначально, существовало 3 разных истории каждая из которых проверяла отдельное состояние формы. Благодаря промежуточным снимкам, удалось сократить общее количество тестов и следовательно снизить общее время их выполнения. При этом, показатель защиты от регресса не постардал.
Вопрос декомпозиции тестов является достаточно комплексным:
- С одной стороны, тестов должно быть как можно меньше, ведь в противном случае растет кол-во кода, увеличивается время выполнения и сами проверки также могут дублировать части друг друга.
- С другой стороны, чем больше тест, тем сложнее контролировать уровень покрытия сценариев работы приложения, к тому же растёт риск смешивания ответственностей с которым борется пункт декомпозиция историй.
Другими словами, дело в балансе.
Общая рекомендация - начинать с самого простого, в большинстве случаев это написание одной крупной истории. Далее, при возникновении проблем с поддержкой её следует разбивать на более атомарные и независимые элементы.
Приоритеты историй
Со временем количество историй в проекте будет расти, вместе с этим будет увеличиваться и время их выполнения. Для того чтобы смягчить влияние данной проблемы, можно объявить приоритет с помощью мета-аттрибутов:
declare module '@storyshots/core' {
interface StoryAttributes<TArgs> {
secondary?: true;
}
}
После, разметить тесты по приоритетам:
const stories = [
// Важно чтобы в приложении работал вход
it('allows to login'),
// При этом, выбор темы относится к второстепенным сценариям
it('allows to set dark theme', {
secondary: true,
}),
];
Далее, установить отдельный режим при котором будут запускаться только важные истории:
run(filter(stories, (story) => not(story.secondary)));
Для того чтобы случайно не исключить лишнего, рекомендуется по умолчанию устанавливать высокий приоритет у историй.
Универсальный render
@storyshots/react предоставляет возможность описывать функцию render у каждой истории по отдельности, что может подойти для
тестирования UI библиотеки:
const renders = it;
const buttonStories = [
renders('primary button', {
render: () => <Button type="primary" />,
}),
renders('primary disabled button', {
render: () => <Button type="primary" disabled />,
}),
];
Однако, для тестирования конечного приложения данный вариант является мало практичным. Вместо этого рекомендуется описывать
render по умолчанию:
export const { run, it } = createPreviewApp(/* ... */);
run(
map(stories, (story) => ({
// По умолчанию будет отрисовываться корневой компонент приложения
render: (externals) => <App externals={externals} />,
...story,
})),
);
export const stories = [
it('...', {
/**
* Описывать render в истории теперь не обязательно.
*/
}),
];