Автоматизированное тестирование будет использоваться в дальнейших задачах, а также широко применяется в реальных проектах.
Когда мы пишем функцию, мы обычно можем представить, что она должна делать: какие параметры дают какие результаты.
Во время разработки мы можем проверить функцию, запустив ее и сравнив результат с ожидаемым. Например, мы можем сделать это в консоли.
Если что-то не так — то исправляем код, запускаем заново, проверяем результат — и так до тех пор, пока не заработает.
Но такие ручные «перепрогоны» несовершенны.
При тестировании кода вручную можно что-то упустить.
Например, мы создаем функцию f
. Написал код, проверяю: f(1)
работает, но f(2)
не работает. Мы исправим код и теперь f(2)
работает. Выглядит завершенным? Но мы забыли повторно протестировать f(1)
. Это может привести к ошибке.
Это очень типично. Когда мы что-то разрабатываем, мы помним множество возможных вариантов использования. Но трудно ожидать, что программист будет проверять их все вручную после каждого изменения. Так становится легко починить одно и сломать другое.
Автоматизированное тестирование означает, что тесты пишутся отдельно, в дополнение к коду. Они запускают наши функции различными способами и сравнивают результаты с ожидаемыми.
Начнем с техники под названием Behavior Driven Development или, короче, BDD.
BDD — это три вещи в одном: тесты И документация И примеры.
Чтобы понять BDD, мы рассмотрим практический пример разработки.
Допустим, мы хотим создать функцию pow(x, n)
которая возводит x
в целую степень n
. Мы предполагаем, что n≥0
.
Эта задача — всего лишь пример: в JavaScript есть оператор **
, который может это сделать, но здесь мы концентрируемся на процессе разработки, который можно применять и к более сложным задачам.
Прежде чем создавать код pow
, мы можем представить, что должна делать функция, и описать ее.
Такое описание называется спецификацией или, короче, спецификацией, и содержит описания вариантов использования вместе с тестами для них, например:
описать("pow", function() { it("возводит в n-ю степень", function() { Assert.equal(pow(2, 3), 8); }); });
Спецификация состоит из трех основных строительных блоков, которые вы можете увидеть выше:
describe("title", function() { ... })
Какую функциональность мы описываем? В нашем случае мы описываем функцию pow
. Используется для группировки «рабочих» — it
.
it("use case description", function() { ... })
В it
мы в понятной для человека форме описываем конкретный вариант использования, а вторым аргументом является функция, которая его тестирует.
assert.equal(value1, value2)
Код внутри it
блока, если реализация правильная, должен выполняться без ошибок.
Функции assert.*
используются для проверки того, работает ли pow
должным образом. Здесь мы используем один из них — assert.equal
, он сравнивает аргументы и выдает ошибку, если они не равны. Здесь он проверяет, что результат pow(2, 3)
равен 8
. Существуют и другие типы сравнений и проверок, которые мы добавим позже.
Спецификация может быть выполнена, и она запустит тест, указанный в it
блоке. Мы увидим это позже.
Ход разработки обычно выглядит так:
Написана первоначальная спецификация с тестами самой базовой функциональности.
Создается первоначальная реализация.
Чтобы проверить, работает ли это, мы запускаем среду тестирования Mocha (подробнее позже), которая запускает эту спецификацию. Пока функционал не полный, отображаются ошибки. Вносим исправления, пока все не заработает.
Теперь у нас есть рабочая первоначальная реализация с тестами.
Мы добавляем в спецификацию больше вариантов использования, возможно, еще не поддерживаемых реализациями. Тесты начинают давать сбои.
Перейдите к шагу 3, обновите реализацию, пока тесты не перестанут выдавать ошибки.
Повторяйте шаги 3–6, пока функционал не будет готов.
Итак, разработка итеративна . Мы пишем спецификацию, реализуем ее, проверяем прохождение тестов, затем пишем еще тесты, проверяем, что они работают и т. д. В конце у нас есть и работающая реализация, и тесты для нее.
Давайте посмотрим на этот поток разработки на нашем практическом примере.
Первый шаг уже завершен: у нас есть начальная спецификация для pow
. Теперь, прежде чем приступить к реализации, давайте воспользуемся несколькими библиотеками JavaScript для запуска тестов, просто чтобы убедиться, что они работают (все они потерпят неудачу).
Здесь, в руководстве, мы будем использовать для тестов следующие библиотеки JavaScript:
Mocha — основная структура: она предоставляет общие функции тестирования, включая describe
и основную it
, которая запускает тесты.
Chai – библиотека со множеством утверждений. Это позволяет использовать множество различных утверждений, сейчас нам нужен только assert.equal
.
Sinon — библиотека для слежки за функциями, эмуляции встроенных функций и многого другого, она нам понадобится гораздо позже.
Эти библиотеки подходят как для тестирования в браузере, так и для тестирования на стороне сервера. Здесь мы рассмотрим браузерный вариант.
Полная HTML-страница с этими фреймворками и спецификациями pow
:
<!DOCTYPE html> <html> <голова> <!-- добавьте CSS Mocha, чтобы показать результаты --> <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.css"> <!-- добавить код фреймворка mocha --> <script src="https://cdnjs.cloudflare.com/ajax/libs/mocha/3.2.0/mocha.js"></script> <скрипт> mocha.setup('бдд'); // минимальная настройка </скрипт> <!-- добавить чай --> <script src="https://cdnjs.cloudflare.com/ajax/libs/chai/3.5.0/chai.js"></script> <скрипт> // в чае много всего, давайте сделаем утверждение глобальным пусть утверждают = chai.assert; </скрипт> </голова> <тело> <скрипт> функция pow(x, n) { /* код функции должен быть записан, сейчас пуст */ } </скрипт> <!-- скрипт с тестами (опишите, это...) --> <script src="test.js"></script> <!-- элемент с id="mocha" будет содержать результаты теста --> <div id="мокко"></div> <!-- запустить тесты! --> <скрипт> мокко.запуск(); </скрипт> </тело> </html>
Страницу можно разделить на пять частей:
<head>
— добавляет сторонние библиотеки и стили для тестов.
<script>
с тестируемой функцией, в нашем случае — с кодом pow
.
Тесты – в нашем случае внешний скрипт test.js
, имеющий describe("pow", ...)
сверху.
HTML-элемент <div id="mocha">
будет использоваться Mocha для вывода результатов.
Тесты запускаются командой mocha.run()
.
Результат:
На данный момент тест не пройден, есть ошибка. Это логично: у нас есть пустой код функции в pow
, поэтому pow(2,3)
возвращает undefined
вместо 8
.
На будущее отметим, что существуют более высокоуровневые тест-раннеры, такие как karma и другие, которые упрощают автозапуск множества различных тестов.
Давайте сделаем простую реализацию pow
для прохождения тестов:
функция pow(x, n) { вернуть 8; // :) обманываем! }
Вау, теперь это работает!
То, что мы сделали, определенно является мошенничеством. Функция не работает: попытка вычислить pow(3,4)
даст неверный результат, но тесты проходят.
…Но ситуация вполне типичная, такое бывает на практике. Тесты проходят, но функция работает неправильно. Наша спецификация несовершенна. Нам нужно добавить к нему больше вариантов использования.
Давайте добавим еще один тест, чтобы проверить, что pow(3, 4) = 81
.
Здесь мы можем выбрать один из двух способов организации теста:
Первый вариант — добавить в тот же it
еще один assert
:
описать("pow", function() { it("возводит в n-ю степень", function() { Assert.equal(pow(2, 3), 8); Assert.equal(pow(3, 4), 81); }); });
Второй – сделать два теста:
описать("pow", function() { it("2 в степени 3 равно 8", function() { Assert.equal(pow(2, 3), 8); }); it("3 в степени 4 равно 81", function() { Assert.equal(pow(3, 4), 81); }); });
Принципиальное отличие состоит в том, что когда assert
вызывает ошибку, блок it
немедленно завершается. Итак, в первом варианте, если первое assert
не удастся, то мы никогда не увидим результат второго assert
.
Разделение тестов полезно для получения дополнительной информации о том, что происходит, поэтому второй вариант лучше.
И кроме этого, есть еще одно правило, которому полезно следовать.
Один тест проверяет одну вещь.
Если мы посмотрим на тест и увидим в нем две независимые проверки, лучше разбить его на две более простые.
Итак, продолжим второй вариант.
Результат:
Как и следовало ожидать, второй тест провалился. Конечно, наша функция всегда возвращает 8
, а assert
ожидает 81
.
Давайте напишем что-нибудь более реальное для прохождения тестов:
функция pow(x, n) { пусть результат = 1; for (пусть я = 0; я < n; я++) { результат *= х; } вернуть результат; }
Чтобы убедиться, что функция работает хорошо, давайте проверим ее на большее количество значений. Вместо того, чтобы it
блоки вручную, мы можем сгенерировать их for
:
описать("pow", function() { функция makeTest(x) { пусть ожидается = х * х * х; it(`${x} в степени 3 равно ${expected}`, function() { Assert.equal(pow(x, 3), ожидается); }); } for (пусть x = 1; x <= 5; x++) { сделатьТест (х); } });
Результат:
Мы собираемся добавить еще больше тестов. Но перед этим отметим, что вспомогательные функции makeTest
и for
следует сгруппировать вместе. В других тестах makeTest
нам не понадобится, он нужен только в for
: их общая задача — проверить, как pow
возводится в заданную степень.
Группировка осуществляется с помощью вложенного describe
:
описать("pow", function() { описать("возводит x в степень 3", function() { функция makeTest(x) { пусть ожидается = х * х * х; it(`${x} в степени 3 равно ${expected}`, function() { Assert.equal(pow(x, 3), ожидается); }); } for (пусть x = 1; x <= 5; x++) { сделатьТест (х); } }); // ... здесь следует выполнить дополнительные тесты, как описывающие, так и их можно добавить });
Вложенное describe
определяет новую «подгруппу» тестов. В выводе мы видим отступ с заголовком:
В будущем мы можем добавить больше и describe
it
на верхнем уровне с помощью собственных вспомогательных функций, они не увидят makeTest
.
before/after
и beforeEach/afterEach
Мы можем настроить функции before/after
, которые выполняются до/после запуска тестов, а также функции beforeEach/afterEach
, которые выполняются до/после каждого it
.
Например:
описать("тест", функция() { before(() => alert("Тестирование началось – до всех тестов"); after(() => alert("Тестирование завершено – после всех тестов"); beforeEach(() => alert("Перед тестом – введите тест"); afterEach(() => alert("После теста – выйти из теста"); it('тест 1', () => alert(1)); it('test 2', () => alert(2)); });
Последовательность выполнения будет такой:
Тестирование началось – до всех тестов (до) Перед тестом – введите тест (beforeEach) 1 После теста – выйти из теста (afterEach) Перед тестом – введите тест (beforeEach) 2 После теста – выйти из теста (afterEach) Тестирование завершено – после всех тестов (после)
Откройте пример в песочнице.
Обычно beforeEach/afterEach
и before/after
используются для выполнения инициализации, обнуления счетчиков или выполнения чего-то еще между тестами (или группами тестов).
Базовая функциональность pow
завершена. Первая итерация разработки завершена. Когда мы закончим праздновать и пить шампанское – продолжим и улучшаем его.
Как было сказано, функция pow(x, n)
предназначена для работы с целыми положительными значениями n
.
Чтобы указать на математическую ошибку, функции JavaScript обычно возвращают NaN
. Давайте сделаем то же самое для недопустимых значений n
.
Давайте сначала добавим поведение в спецификацию(!):
описать("pow", function() { // ... it("для отрицательного n результат будет NaN", function() { Assert.isNaN(pow(2, -1)); }); it("для нецелого n результатом будет NaN", function() { Assert.isNaN(pow(2, 1.5)); }); });
Результат с новыми тестами:
Недавно добавленные тесты завершаются неудачей, поскольку наша реализация их не поддерживает. Вот как делается BDD: сначала пишем провальные тесты, а потом делаем для них реализацию.
Другие утверждения
Обратите внимание на утверждение assert.isNaN
: оно проверяет NaN
.
В Чай есть и другие утверждения, например:
assert.equal(value1, value2)
– проверяет равенство value1 == value2
.
assert.strictEqual(value1, value2)
– проверяет строгое равенство value1 === value2
.
assert.notEqual
, assert.notStrictEqual
– проверки, обратные приведенным выше.
assert.isTrue(value)
– проверяет, что value === true
assert.isFalse(value)
– проверяет, что value === false
…полный список находится в документации
Поэтому нам нужно добавить пару строк в pow
:
функция pow(x, n) { если (n <0) вернуть NaN; if (Math.round(n) != n) вернуть NaN; пусть результат = 1; for (пусть я = 0; я < n; я++) { результат *= х; } вернуть результат; }
Теперь всё работает, все тесты пройдены:
Откройте полный финальный пример в песочнице.
В BDD сначала идет спецификация, а затем реализация. В конце у нас есть и спецификация, и код.
Спецификацию можно использовать тремя способами:
В качестве тестов — они гарантируют, что код работает корректно.
В документации — заголовки describe
и it
, что делает функция.
В качестве примеров — тесты на самом деле являются рабочими примерами, показывающими, как можно использовать функцию.
С помощью спецификации мы можем безопасно улучшить, изменить и даже переписать функцию с нуля и убедиться, что она по-прежнему работает правильно.
Это особенно важно в больших проектах, когда функция используется во многих местах. Когда мы меняем такую функцию, просто нет возможности вручную проверить, работает ли все места, где она используется, правильно.
Без тестов у людей есть два пути:
Чтобы выполнить изменение, несмотря ни на что. И тогда наши пользователи сталкиваются с ошибками, так как мы, вероятно, не успеваем что-то проверить вручную.
Или, если наказание за ошибки жесткое, так как нет тестов, люди начинают бояться модифицировать такие функции, и тогда код устаревает, никто не хочет в него лезть. Нехорошо для развития.
Автоматическое тестирование помогает избежать этих проблем!
Если проект покрыт тестами, такой проблемы просто нет. После любых изменений мы можем запустить тесты и увидеть множество проверок, выполненных за считанные секунды.
Кроме того, хорошо протестированный код имеет лучшую архитектуру.
Естественно, это потому, что автоматически протестированный код легче модифицировать и улучшать. Но есть и другая причина.
Для написания тестов код должен быть организован таким образом, чтобы каждая функция имела четко описанную задачу, четко определенные входные и выходные данные. Это означает хорошую архитектуру с самого начала.
В реальной жизни это иногда не так просто. Иногда сложно написать спецификацию до написания реального кода, потому что еще не ясно, как она должна себя вести. Но в целом написание тестов делает разработку быстрее и стабильнее.
Далее в руководстве вы встретите множество задач со встроенными тестами. Так вы увидите больше практических примеров.
Написание тестов требует хорошего знания JavaScript. Но мы только начинаем этому учиться. Итак, чтобы во всем разобраться, на данный момент вам не обязательно писать тесты, но вы уже должны уметь их читать, даже если они немного сложнее, чем в этой главе.
важность: 5
Что не так в тесте pow
ниже?
it("Возводит x в степень n", function() { пусть х = 5; пусть результат = х; Assert.equal(pow(x, 1), результат); результат *= х; Assert.equal(pow(x, 2), результат); результат *= х; Assert.equal(pow(x, 3), результат); });
PS Синтаксически тест корректен и проходит.
Тест демонстрирует один из соблазнов, с которым сталкивается разработчик при написании тестов.
На самом деле у нас есть три теста, но они представлены как одна функция с тремя утверждениями.
Иногда проще написать так, но если возникает ошибка, то гораздо менее очевидно, что пошло не так.
Если ошибка произойдет в середине сложного потока выполнения, нам придется выяснить данные в этой точке. На самом деле нам придется отлаживать тест .
Было бы гораздо лучше разбить тест на it
блоков с четко написанными входными и выходными данными.
Так:
описать("Возводит x в степень n", function() { it("5 в степени 1 равно 5", function() { Assert.equal(pow(5, 1), 5); }); it("5 в степени 2 равно 25", function() { Assert.equal(pow(5, 2), 25); }); it("5 в степени 3 равно 125", function() { Assert.equal(pow(5, 3), 125); }); });
Мы заменили один it
на describe
и группу it
блоков. Теперь, если что-то потерпит неудачу, мы сможем ясно увидеть, какие это были данные.
Также мы можем изолировать один тест и запустить его в автономном режиме, написав вместо it
it.only
:
описать("Возводит x в степень n", function() { it("5 в степени 1 равно 5", function() { Assert.equal(pow(5, 1), 5); }); // Mocha будет запускать только этот блок it.only("5 в степени 2 равно 25", function() { Assert.equal(pow(5, 2), 25); }); it("5 в степени 3 равно 125", function() { Assert.equal(pow(5, 3), 125); }); });