Эта статья была рецензирована Panayiotis «pvgr» Велисаракос , Марк Браун и Том Греко . Спасибо всем рецензентам SitePoint за то, что сделали контент SitePoint как можно лучше!
Вы когда-нибудь вносили какие-то изменения в свой код, а потом обнаружили, что он вызвал что-то еще?
Я уверен, что большинство из нас имеют. Это почти неизбежно, особенно когда у вас больше кода. Одна вещь зависит от другой, и тогда ее изменение нарушает что-то другое.
Но что, если этого не произошло? Что если бы у вас был способ узнать, когда что-то сломалось в результате какого-то изменения? Это было бы здорово. Вы можете изменить свой код, не беспокоясь о том, что что-то может сломаться, у вас будет меньше ошибок и вы потратите меньше времени на отладку.
Вот где блестят юнит-тесты. Они автоматически обнаружат любые проблемы в коде для вас. Внесите изменения, запустите свои тесты и, если что-то сломается, вы сразу узнаете, что произошло, где проблема и каково должно быть правильное поведение. Это полностью исключает любые догадки!
В этой статье я покажу вам, как начать модульное тестирование кода JavaScript. Примеры и методы, показанные в этой статье, могут применяться как к браузерному коду, так и к коду Node.js.
Код для этого урока доступен в нашем репозитории GitHub .
Что такое юнит-тестирование
Когда вы тестируете свою кодовую базу, вы берете кусок кода — обычно функцию — и проверяете его правильное поведение в конкретной ситуации. Модульное тестирование — это структурированный и автоматизированный способ сделать это. В результате, чем больше тестов вы пишете, тем больше вы получаете выгоды. Вы также будете более уверенно доверять своей кодовой базе, продолжая ее развивать.
Основная идея модульного тестирования заключается в проверке поведения функции при предоставлении ей определенного набора входных данных. Вы вызываете функцию с определенными параметрами и проверяете, получили ли вы правильный результат.
// Given 1 and 10 as inputs... var result = Math.max(1, 10); // ...we should receive 10 as the output if(result !== 10) { throw new Error('Failed'); }
На практике тесты иногда могут быть более сложными. Например, если ваша функция отправляет запрос Ajax, тесту необходимо настроить больше, но тот же принцип «при определенных входных данных, мы ожидаем определенного результата» все еще применяется.
Настройка инструментов
Для этой статьи мы будем использовать Мокко. С ним легко начать, его можно использовать как для тестирования на основе браузера, так и для тестирования Node.js, и он прекрасно сочетается с другими инструментами тестирования.
Самый простой способ установить Mocha — через npm (для этого нам также нужно установить Node.js ). Если вы не знаете, как установить npm или Node в своей системе, обратитесь к нашему руководству: Руководство для начинающих по npm — менеджер пакетов Node
С установленным Node откройте терминал или командную строку в каталоге вашего проекта.
- Если вы хотите проверить код в браузере, запустите
npm install mocha chai --save-dev
- Если вы хотите протестировать код Node.js, в дополнение к вышеупомянутому, запустите
npm install -g mocha
Это устанавливает пакеты mocha
и chai
. Mocha — это библиотека, которая позволяет нам запускать тесты, а Chai содержит некоторые полезные функции, которые мы будем использовать для проверки результатов наших тестов.
Тестирование на Node.js против тестирования в браузере
Следующие примеры предназначены для работы при запуске тестов в браузере. Если вы хотите выполнить модульное тестирование приложения Node.js, выполните следующие действия.
- Для Node вам не нужен файл запуска теста.
- Чтобы включить Chai, добавьте
var chai = require('chai');
в верхней части тестового файла. - Запустите тесты с помощью команды
mocha
вместо открытия браузера.
Настройка структуры каталогов
Вы должны поместить свои тесты в отдельный каталог из файлов основного кода. Это облегчает их структурирование, например, если вы хотите добавить другие типы тестов в будущем (например, интеграционные тесты или функциональные тесты ).
Самая популярная практика с JavaScript-кодом — иметь каталог с именем test/
в корневом каталоге вашего проекта. Затем каждый тестовый файл помещается в test/someModuleTest.js
. При желании вы также можете использовать каталоги внутри test/
, но я рекомендую сделать вещи простыми — вы всегда можете изменить их позже, если это необходимо.
Настройка Test Runner
Чтобы запустить наши тесты в браузере, нам нужно настроить простую HTML-страницу, которая будет нашей страницей. Страница загружает Mocha, тестовые библиотеки и наши актуальные тестовые файлы. Чтобы запустить тесты, мы просто откроем бегуна в браузере.
Если вы используете Node.js, вы можете пропустить этот шаг. Модульные тесты Node.js можно запустить с помощью команды mocha
, если вы следовали рекомендуемой структуре каталогов.
Ниже приведен код, который мы будем использовать для тестового бегуна. Я testrunner.html
этот файл как testrunner.html
.
<!DOCTYPE html> <html> <head> <title>Mocha Tests</title> <link rel="stylesheet" href="node_modules/mocha/mocha.css"> </head> <body> <div id="mocha"></div> <script src="node_modules/mocha/mocha.js"></script> <script src="node_modules/chai/chai.js"></script> <script>mocha.setup('bdd')</script> <!-- load code you want to test here --> <!-- load your test files here --> <script> mocha.run(); </script> </body> </html>
Важные биты в тесте бегуна:
- Мы загружаем стили Mocha CSS, чтобы дать нашим результатам тестов хорошее форматирование.
- Мы создаем div с идентификатором
mocha
. Это где результаты теста вставляются. - Мы загружаем мокко и чай. Они находятся в подпапках папки
node_modules
так как мы установили их через npm. -
mocha.setup
, мы предоставляем помощников по тестированию Mocha. - Затем мы загружаем код, который мы хотим проверить, и тестовые файлы. У нас здесь пока ничего нет.
- Наконец, мы вызываем
mocha.run
для запуска тестов. Убедитесь, что вы вызываете это после загрузки исходного и тестового файлов.
Основные тестовые блоки
Теперь, когда мы можем запускать тесты, давайте начнем писать.
Начнем с создания нового файла test/arrayTest.js
. Отдельный файл теста, такой как этот, называется тестовым примером . Я называю это arrayTest.js
потому что для этого примера мы будем тестировать некоторые базовые функции массива.
Каждый файл теста соответствует одному и тому же базовому шаблону. Во-первых, у вас есть блок describe
:
describe('Array', function() { // Further code for tests goes here });
describe
используется для группировки отдельных тестов. Первый параметр должен указывать то, что мы тестируем — в этом случае, так как мы собираемся протестировать функции массива, я передал строку 'Array'
.
Во-вторых, внутри describe
у нас будут блоки:
describe('Array', function() { it('should start empty', function() { // Test implementation goes here }); // We can have more its here });
it
используется для создания реальных тестов. Первый параметр к it
должен предоставить удобочитаемое описание теста. Например, мы можем прочитать вышеизложенное как «оно должно начинаться с пустого», что является хорошим описанием того, как должны вести себя массивы. Код для реализации теста затем пишется внутри переданной it
.
Все тесты Mocha строятся из тех же самых строительных блоков, и они следуют той же базовой схеме.
- Во-первых, мы используем
describe
чтобы сказать, что мы тестируем — например, «опишите, как должен работать массив». - Затем мы используем ряд
it
функций для создания отдельных тестов — каждый изit
должен объяснять одно конкретное поведение, такое как «оно должно начинаться с пустого» для нашего случая массива выше.
Написание тестового кода
Теперь, когда мы знаем, как структурировать тестовый пример, давайте перейдем к забавной части — реализации теста.
Поскольку мы проверяем, что массив должен начинаться с пустого места, нам нужно создать массив, а затем убедиться, что он пуст. Реализация этого теста довольно проста:
var assert = chai.assert; describe('Array', function() { it('should start empty', function() { var arr = []; assert.equal(arr.length, 0); }); });
Обратите внимание, что в первой строке мы устанавливаем переменную assert
. Это просто так, что нам не нужно постоянно вводить chai.assert
везде.
В функции it
мы создаем массив и проверяем его длину. Несмотря на простоту, это хороший пример того, как работают тесты.
Во-первых, у вас есть что-то, что вы тестируете — это называется тестируемой системой или SUT . Затем, если необходимо, вы делаете что-то с SUT. В этом тесте мы ничего не делаем, так как проверяем, что массив начинается как пустой.
Последней вещью в тесте должна быть проверка — утверждение, которое проверяет результат. Здесь мы используем assert.equal
для этого. Большинство функций утверждений принимают параметры в том же порядке: сначала «фактическое» значение, а затем «ожидаемое» значение.
- Фактическое значение является результатом вашего тестового кода, поэтому в этом случае
arr.length
- Ожидаемое значение — это то, каким должен быть результат. Поскольку массив должен начинаться с пустого значения, ожидаемое значение в этом тесте равно
0
Чай также предлагает два разных стиля написания утверждений, но сейчас мы используем assert, чтобы упростить задачу. Когда вы станете более опытным в написании тестов, вы можете вместо этого использовать ожидаемые утверждения , поскольку они обеспечивают большую гибкость.
Выполнение теста
Чтобы запустить этот тест, нам нужно добавить его в файл, созданный ранее.
Если вы используете Node.js, вы можете пропустить этот шаг и использовать команду mocha
для запуска теста. Вы увидите результаты теста в терминале.
В противном случае, чтобы добавить этот тест для бегуна, просто добавьте:
<script src="test/arrayTest.js"></script>
Ниже:
<!-- load your test files here -->
После того, как вы добавили скрипт, вы можете загрузить страницу бегуна тестов в выбранном вами браузере.
Результаты теста
Когда вы запустите свои тесты, результаты теста будут выглядеть примерно так:
Обратите внимание, что то, что мы ввели в describe
и it
функции отображаются в выходных данных — тесты сгруппированы под описанием. Обратите внимание, что также возможно вложить блоки describe
для создания дополнительных подгрупп.
Давайте посмотрим, как выглядит провальный тест.
На строке в тесте, которая говорит:
assert.equal(arr.length, 0);
Замените число 0
на 1
. Это делает тест неудачным, так как длина массива больше не соответствует ожидаемому значению.
Если вы снова запустите тесты, вы увидите провальный тест красным цветом с описанием того, что пошло не так.
Одним из преимуществ тестов является то, что они помогают быстрее находить ошибки, однако эта ошибка не очень полезна в этом отношении. Мы можем это исправить, хотя.
Большинство функций утверждения также могут принимать необязательный параметр message
. Это сообщение, которое отображается при сбое подтверждения. Рекомендуется использовать этот параметр, чтобы облегчить понимание сообщения об ошибке.
Мы можем добавить сообщение к нашему утверждению следующим образом:
assert.equal(arr.length, 1, 'Array length was not 0');
Если вы повторно запустите тесты, вместо сообщения по умолчанию появится пользовательское сообщение.
Давайте переключим утверждение обратно на прежнее состояние — заменим 1
на 0
и снова запустим тесты, чтобы убедиться, что они пройдены.
Положить его вместе
До сих пор мы рассматривали довольно простые примеры. Давайте применим на практике то, что мы узнали, и посмотрим, как мы будем тестировать более реалистичный фрагмент кода.
Вот функция, которая добавляет класс CSS к элементу. Это должно войти в новый файл js/className.js
.
function addClass(el, newClass) { if(el.className.indexOf(newClass) === -1) { el.className += newClass; } }
Чтобы сделать его немного интереснее, я сделал так, чтобы он добавлял новый класс только тогда, когда этот класс не существует в свойстве className
элемента — кто хочет увидеть <div class="hello hello hello hello">
конце концов?
В лучшем случае мы написали бы тесты для этой функции, прежде чем писать код. Но разработка через тестирование — сложная тема, и сейчас мы просто хотим сосредоточиться на написании тестов.
Для начала, давайте вспомним основную идею модульных тестов: мы даем функции определенные входные данные, а затем проверяем, что функция ведет себя как ожидалось. Итак, каковы входы и поведение для этой функции?
Учитывая элемент и имя класса:
- если свойство
className
элемента не содержит имя класса, его следует добавить. - если свойство
className
элемента содержит имя класса, его не следует добавлять.
Давайте переведем эти случаи в два теста. В test
каталоге создайте новый файл classNameTest.js
и добавьте следующее:
describe('addClass', function() { it('should add class to element'); it('should not add a class which already exists'); });
Мы немного изменили формулировку на форму «она должна делать X», используемую в тестах. Это означает, что он читается немного лучше, но по сути остается той же читабельной формой, которую мы перечислили выше. Обычно не намного сложнее, чем перейти от идеи к тесту.
Но подождите, где тестовые функции? Что ж, когда мы опускаем второй параметр, Mocha помечает эти тесты как ожидающие в результатах теста. Это удобный способ настроить ряд тестов — что-то вроде списка задач, которые вы собираетесь написать.
Давайте продолжим реализацию первого теста.
describe('addClass', function() { it('should add class to element', function() { var element = { className: '' }; addClass(element, 'test-class'); assert.equal(element.className, 'test-class'); }); it('should not add a class which already exists'); });
В этом тесте мы создаем переменную element
и передаем ее в качестве параметра функции addClass
вместе со строкой test-class
(новый класс для добавления). Затем мы проверяем, что класс включен в значение, используя утверждение.
Опять же, мы пошли от нашей первоначальной идеи — учитывая элемент и имя класса, его следует добавить в список классов — и довольно просто перевели его в код.
Хотя эта функция предназначена для работы с элементами DOM, мы используем простой объект JS. Иногда мы можем использовать динамическую природу JavaScript таким образом, чтобы упростить наши тесты. Если бы мы этого не делали, нам нужно было бы создать фактический элемент, и это усложнило бы наш тестовый код. В качестве дополнительного преимущества, поскольку мы не используем DOM, мы также можем запустить этот тест в Node.js, если захотим.
Запуск тестов в браузере
Чтобы запустить тест в браузере, вам нужно добавить className.js
и classNameTest.js
к исполнителю:
<!-- load code you want to test here --> <script src="js/className.js"></script> <!-- load your test files here --> <script src="test/classNameTest.js"></script>
Теперь вы должны увидеть, что один тестовый проход и другой тест отображаются как ожидающие, как показано в следующем CodePen. Обратите внимание, что код немного отличается от примера, чтобы код работал в среде CodePen.
Далее давайте реализуем второй тест …
it('should not add a class which already exists', function() { var element = { className: 'exists' }; addClass(element, 'exists'); var numClasses = element.className.split(' ').length; assert.equal(numClasses, 1); });
Это хорошая привычка часто запускать ваши тесты, поэтому давайте проверим, что произойдет, если мы запустим тесты сейчас. Как и следовало ожидать, они должны пройти.
Вот еще один CodePen со вторым реализованным тестом.
Но держись! Я на самом деле немного обманул тебя. Для этой функции существует третье поведение, которое мы не рассмотрели. В функции также есть ошибка — довольно серьезная. Это только трехстрочная функция, но вы заметили это?
Давайте напишем еще один тест для третьего поведения, который выставляет ошибку в качестве бонуса.
it('should append new class after existing one', function() { var element = { className: 'exists' }; addClass(element, 'new-class'); var classes = element.className.split(' '); assert.equal(classes[1], 'new-class'); });
На этот раз тест не пройден. Вы можете увидеть это в действии в следующем CodePen. Проблема здесь проста: имена классов CSS в элементах должны быть разделены пробелом. Однако наша текущая реализация addClass
не добавляет пробела!
Давайте исправим функцию и сделаем тест пройден.
function addClass(el, newClass) { if(el.className.indexOf(newClass) !== -1) { return; } if(el.className !== '') { //ensure class names are separated by a space newClass = ' ' + newClass; } el.className += newClass; }
И вот последний CodePen с фиксированной функцией и прохождением тестов.
Выполнение тестов на узле
В Node вещи видны только другим вещам в том же файле. Так как className.js
и classNameTest.js
находятся в разных файлах, нам нужно найти способ показать один другому. Стандартный способ сделать это — использовать module.exports
. Если вам нужен освежающий напиток , вы можете прочитать все об этом здесь: Понимание module.exports и экспорта в Node.js
Код по существу остается тем же самым, но структурирован немного по-другому:
// className.js module.exports = { addClass: function(el, newClass) { if(el.className.indexOf(newClass) !== -1) { return; } if(el.className !== '') { //ensure class names are separated by a space newClass = ' ' + newClass; } el.className += newClass; } }
// classNameTest.js var chai = require('chai'); var assert = chai.assert; var className = require('../js/className.js'); var addClass = className.addClass; // The rest of the file remains the same describe('addClass', function() { ... });
И, как видите, тесты пройдены.
Что дальше?
Как видите, тестирование не должно быть сложным или сложным. Как и в случае с другими аспектами написания приложений на JavaScript, у вас есть несколько базовых шаблонов, которые повторяются. Как только вы познакомитесь с ними, вы можете продолжать использовать их снова и снова.
Но это всего лишь царапина на поверхности. Есть много чего узнать о модульном тестировании.
- Тестирование более сложных систем
- Как работать с Ajax, базами данных и другими «внешними» вещами?
- Разработка через тестирование
Если вы хотите продолжить изучать это и многое другое, я создал бесплатную серию быстрого запуска модульного тестирования JavaScript . Если вы нашли эту статью полезной, вам обязательно стоит ознакомиться с ней здесь .
В качестве альтернативы, если видео больше подходит вашему стилю, вас может заинтересовать курс SitePoint Premium: разработка через тестирование в Node.js.