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

Истории

История это базовый элемент 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 разных истории каждая из которых проверяла отдельное состояние формы. Благодаря промежуточным снимкам, удалось сократить общее количество тестов и следовательно снизить общее время их выполнения. При этом, показатель защиты от регресса не постардал.

примечание

Вопрос декомпозиции тестов является достаточно комплексным:

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

Другими словами, дело в балансе.

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

Приоритеты историй

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

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

extend-module.ts
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 по умолчанию:

preview.tsx
export const { run, it } = createPreviewApp(/* ... */);
index.tsx
run(
map(stories, (story) => ({
// По умолчанию будет отрисовываться корневой компонент приложения
render: (externals) => <App externals={externals} />,
...story,
})),
);
stories.tsx
export const stories = [
it('...', {
/**
* Описывать render в истории теперь не обязательно.
*/
}),
];