Изученной информации уже достаточно для тестирования в повседневной практике разработки. Перед тем, как погружаться в более сложные темы и возможности 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()
})
[Но не всегда пограничные случаи так легко увидеть. Маловероятно, что любой программист сможет сразу написать все нужные тесты. Если в коде возникла ошибка, для которой не было теста, то сначала напишите тест, который воспроизводит эту ошибку, и затем уже чините ее. Только так можно поддерживать достаточный уровень надежности, не превращая разработку в непрерывную починку багов.
Дополнительные материалы
Для полного доступа к курсу нужен базовый план
Базовый план откроет полный доступ ко всем курсам, упражнениям и урокам Хекслета, проектам и пожизненный доступ к теории пройденных уроков. Подписку можно отменить в любой момент.