Зарегистрируйтесь, чтобы продолжить обучение

Модульные тесты JS: Автоматическое тестирование

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

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

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

import makeStack from '../src/stack.js'

const stack = makeStack()
stack.isEmpty() // true
stack.push(1) // (1)
stack.push(2) // (1, 2)
stack.push(3) // (1, 2, 3)
stack.isEmpty() // false
stack.pop() // 3. В стеке (1, 2)
stack.pop() // 2. В стеке (1)
stack.pop() // 1. В стеке пусто
stack.isEmpty() // true

Сначала решим организационные вопросы. Если предположить, что реализация стека лежит в файле src/stack.js, то его тест мы положим в файл __tests__/stack.test.js.

Тестируем основную функциональность

Теперь напишем первый тест. Первый тест всегда должен проверять позитивный сценарий — тот, в котором задействована основная функциональность тестируемого компонента:

import makeStack from '../src/stack.js'

test('stack\'s main flow', () => {
  const stack = makeStack()
  // Добавляем два элемента в стек и затем извлекаем их
  stack.push('one')
  stack.push('two')
  expect(stack.pop()).toEqual('two')
  expect(stack.pop()).toEqual('one')
})

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

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

test('stack\'s main flow', () => {
  const stack = makeStack()
  stack.push('one')
  stack.push('two')
  expect(stack.pop()).toEqual('two')
})

test('stack\'s main flow', () => {
  const stack = makeStack()
  stack.push('one')
  stack.push('two')
  stack.pop()
  expect(stack.pop()).toEqual('one')
})

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

Тестируем дополнительную функциональность

Следующим тестом будет тест на дополнительные функции стека. К таким у нас относится функция isEmpty(), которая проверяет, пустой ли стек:

test('isEmpty', () => {
  const stack = makeStack()
  expect(stack.isEmpty()).toBe(true)
  stack.push('two')
  expect(stack.isEmpty()).toBe(false)
  stack.pop()
  expect(stack.isEmpty()).toBe(true)
})

В этом тесте проверяются сразу три ситуации:

  • начальное состояние стека
  • состояние стека после добавления элементов
  • состояние стека после извлечения всех элементов

В принципе, этого достаточно. Хотя в теории возможны ситуации, при которых isEmpty() все равно сломается. Нужно ли пытаться найти все варианты? Не нужно. Тесты не даются бесплатно, каждая написанная строчка кода в проекте — потенциальное место для изменения в случае правок. Если есть сомнения, нужно ли писать проверку или нет, то лучше не пишите. Так вы поймете тот минимум, который стоит писать, и после которого тесты писать не эффективно. Редкие ситуации требуют покрытия тестами только тогда, когда они критичны для работоспособности.

Пограничные случаи

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

test('pop in empty stack', () => {
  const stack = makeStack()
  // Вызов метода pop обернут в функцию
  // Иначе матчер не сможет перехватить исключение
  expect(() => stack.pop()).toThrow()
})

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


Дополнительные материалы

  1. Чек-лист хороших инженерных практик в компаниях

Для полного доступа к курсу нужен базовый план

Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.

Получить доступ
1000
упражнений
2000+
часов теории
3200
тестов

Открыть доступ

Курсы программирования для новичков и опытных разработчиков. Начните обучение бесплатно

  • 130 курсов, 2000+ часов теории
  • 1000 практических заданий в браузере
  • 360 000 студентов
Отправляя форму, вы принимаете «Соглашение об обработке персональных данных» и условия «Оферты», а также соглашаетесь с «Условиями использования»

Наши выпускники работают в компаниях:

Логотип компании Альфа Банк
Логотип компании Aviasales
Логотип компании Yandex
Логотип компании Tinkoff