Один тест - один сценарий

Тестирование компонентов в JavaScript становится более управляемым и эффективным, когда соблюдается принцип «Один тест — один сценарий». Этот принцип основывается на том, что каждый тест должен проверять только один функциональный аспект компонента или приложения, чтобы избежать путаницы и снизить вероятность ошибок.

Почему принцип «Один тест — один сценарий» важен?

Упрощение отладки

Когда в тесте проверяются сразу несколько сценариев, проблемы с кодом могут быть сложными для выявления. Например, если тест не проходит, сложно определить, какой из сценариев оказался некорректным. Принцип «Один тест — один сценарий» помогает изолировать проблему и быстрее выявить источник ошибки.

Легкость в поддержке

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

Повышение читаемости тестов

Тесты становятся более читаемыми и понятными, когда каждый тест описывает один конкретный сценарий. Принцип «Один тест — один сценарий» позволяет ясно структурировать тесты, делая их самодокументированными.

Снижение зависимости тестов

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

Как реализовать принцип «Один тест — один сценарий»?

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

1. Четкое разделение тестов

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

Пример неправильного подхода:

it('should render correctly and handle button click', () => {
  const wrapper = shallow(<MyComponent />);
  expect(wrapper.find('button')).toHaveLength(1);
  
  wrapper.find('button').simulate('click');
  expect(wrapper.state().clicked).toBe(true);
});

Этот тест проверяет сразу два сценария: рендеринг компонента и обработку клика. Лучше разделить его на два отдельных теста:

Пример правильного подхода:

it('should render button correctly', () => {
  const wrapper = shallow(<MyComponent />);
  expect(wrapper.find('button')).toHaveLength(1);
});

it('should handle button click correctly', () => {
  const wrapper = shallow(<MyComponent />);
  wrapper.find('button').simulate('click');
  expect(wrapper.state().clicked).toBe(true);
});

2. Использование Mock и Spy

В случае, если тестируемый компонент взаимодействует с другими компонентами или внешними библиотеками, важно минимизировать зависимости в тестах. Для этого можно использовать моки или шпионов (spies). Это позволяет протестировать функциональность компонента в изоляции.

Пример:

it('should call the onClick handler when button is clicked', () => {
  const onClickM ock = jest.fn();
  const wrapper = shallow(<MyComponent onCl ick={onClickMock} />);
  
  wrapper.find('button').simulate('click');
  
  expect(onClickMock).toHaveBeenCalled();
});

3. Разделение сложных тестов

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

Пример неправильного подхода:

it('should toggle between states correctly', () => {
  const wrapper = shallow(<MyComponent />);
  
  wrapper.find('button').simulate('click');
  expect(wrapper.state().toggled).toBe(true);
  
  wrapper.find('button').simulate('click');
  expect(wrapper.state().toggled).toBe(false);
});

Этот тест проверяет несколько состояний компонента в одном тесте. Лучше будет разделить его на два отдельных:

Пример правильного подхода:

it('should toggle to true when button is clicked', () => {
  const wrapper = shallow(<MyComponent />);
  wrapper.find('button').simulate('click');
  expect(wrapper.state().toggled).toBe(true);
});

it('should toggle to false when button is clicked again', () => {
  const wrapper = shallow(<MyComponent />);
  wrapper.find('button').simulate('click');
  wrapper.find('button').simulate('click');
  expect(wrapper.state().toggled).toBe(false);
});

4. Избежание сложных условий в тестах

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

Пример:

it('should render correctly depending on props', () => {
  const wrapper1 = shallow(<MyComponent isActive={true} />);
  expect(wrapper1.find('.active')).toHaveLength(1);

  const wrapper2 = shallow(<MyComponent isActive={false} />);
  expect(wrapper2.find('.inactive')).toHaveLength(1);
});

Этот тест проверяет два разных сценария рендеринга в одном тесте. Лучше будет разделить его на два:

Пример:

it('should render active class when isActive is true', () => {
  const wrapper = shallow(<MyComponent isActive={true} />);
  expect(wrapper.find('.active')).toHaveLength(1);
});

it('should render inactive class when isActive is false', () => {
  const wrapper = shallow(<MyComponent isActive={false} />);
  expect(wrapper.find('.inactive')).toHaveLength(1);
});

5. Использование асинхронных тестов

Когда компонент использует асинхронные операции, например, запросы к серверу или таймеры, важно, чтобы тесты правильно ожидали завершения этих операций. Для этого можно использовать асинхронные методы Jest, такие как async/await или done.

Пример:

it('should fetch data and update state', async () => {
  const wrapper = shallow(<MyComponent />);
  await wrapper.instance().fetchData();
  expect(wrapper.state().data).toBeDefined();
});

Этот тест проверяет только один сценарий — завершение асинхронной операции и обновление состояния.

Преимущества принципа «Один тест — один сценарий»

  • Легкость в поддержке: Каждый тест становится проще и быстрее обновлять, когда требуется изменить логику компонента.
  • Чистота тестов: Тесты становятся более читаемыми, и любые изменения в поведении компонента можно отслеживать на уровне отдельного теста.
  • Меньше ошибок: Когда тест проверяет только один сценарий, вероятность ошибки уменьшается, так как каждый аспект тестируется в изоляции.
  • Поддержка больших проектов: Принцип «Один тест — один сценарий» позволяет легко масштабировать тесты, особенно в крупных проектах с большим количеством компонентов.

Заключение

Принцип «Один тест — один сценарий» является важным для написания качественных и поддерживаемых тестов. Он помогает организовать тесты в удобочитаемом формате, улучшить их поддержку и упрощает выявление ошибок. Следование этому принципу позволяет разработчикам сосредоточиться на одном аспекте компонента, избегая путаницы и сложности, которые могут возникнуть при проверке нескольких сценариев в одном тесте.