This commit is contained in:
Ilya Kantor 2016-03-04 19:06:22 +03:00
parent e78e527866
commit 05a93ced80
212 changed files with 3213 additions and 3968 deletions

View file

@ -1,12 +1,14 @@
# Сделать pow по спецификации
importance: 5
[importance 5]
---
# Сделать pow по спецификации
Исправьте код функции `pow`, чтобы тесты проходили.
Для этого ниже в задаче вы найдёте ссылку на песочницу.
Она содержит HTML с тестами. Обратите внимание, что HTML-страница в ней короче той, что обсуждалась в статье [](/testing). Это потому что библиотеки Chai, Mocha и Sinon объединены в один файл:
Она содержит HTML с тестами. Обратите внимание, что HTML-страница в ней короче той, что обсуждалась в статье <info:testing>. Это потому что библиотеки Chai, Mocha и Sinon объединены в один файл:
```html
<script src="https://js.cx/test/libs.js"></script>

View file

@ -6,7 +6,7 @@ it("любое число в степени 0 равно 1", function() {
});
```
Конечно, желательно проверить на нескольких числах.
Конечно, желательно проверить на нескольких числах.
Поэтому лучше будет создать блок `describe`, аналогичный тому, что мы делали для произвольных чисел:
@ -28,8 +28,7 @@ describe("любое число, кроме нуля, в степени 0 рав
И не забудем добавить отдельный тест для нуля:
```js
//+ no-beautify
```js no-beautify
...
it("ноль в нулевой степени даёт NaN", function() {
assert( isNaN(pow(0, 0)), "0 в степени 0 не NaN");

View file

@ -1,6 +1,8 @@
# Добавьте тест к задаче
importance: 5
[importance 5]
---
# Добавьте тест к задаче
Добавьте к [предыдущей задаче](/task/pow-nan-spec) тесты, которые будут проверять, что любое число, кроме нуля, в нулевой степени равно `1`, а ноль в нулевой степени даёт `NaN` (это математически корректно, результат 0<sup>0</sup> не определён).

View file

@ -1,6 +1,6 @@
Этот тест демонстрирует один из соблазнов, которые ожидают начинающего автора тестов.
Вместо того, чтобы написать три различных теста, он изложил их в виде одного потока вычислений, с несколькими `assert`.
Вместо того, чтобы написать три различных теста, он изложил их в виде одного потока вычислений, с несколькими `assert`.
Иногда так написать легче и проще, однако при ошибке в тесте гораздо менее очевидно, что же пошло не так.

View file

@ -1,6 +1,8 @@
# Что не так в тесте?
importance: 5
[importance 5]
---
# Что не так в тесте?
Что не так в этом тесте функции `pow`?

View file

@ -8,9 +8,9 @@
При написании функции мы обычно представляем, что она должна делать, какое значение -- на каких аргументах выдавать.
В процессе разработки мы, время от времени, проверяем, правильно ли работает функция. Самый простой способ проверить -- это запустить её, например, в консоли, и посмотреть результат.
В процессе разработки мы, время от времени, проверяем, правильно ли работает функция. Самый простой способ проверить -- это запустить её, например, в консоли, и посмотреть результат.
Если что-то не так -- поправить, опять запустить -- посмотреть результат... И так -- "до победного конца".
Если что-то не так -- поправить, опять запустить -- посмотреть результат... И так -- "до победного конца".
Но такие ручные запуски -- очень несовершенное средство проверки.
@ -22,19 +22,18 @@
## BDD -- поведенческие тесты кода
Мы рассмотрим методику тестирования, которая входит в [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development) -- Behavior Driven Development. Подход BDD давно и с успехом используется во многих проектах.
Мы рассмотрим методику тестирования, которая входит в [BDD](http://en.wikipedia.org/wiki/Behavior-driven_development) -- Behavior Driven Development. Подход BDD давно и с успехом используется во многих проектах.
BDD -- это не просто тесты. Это гораздо больше.
BDD -- это не просто тесты. Это гораздо больше.
**Тесты BDD -- это три в одном: И тесты И документация И примеры использования одновременно.**
Впрочем, хватит слов. Рассмотрим примеры.
## Разработка pow: спецификация
## Разработка pow: спецификация
Допустим, мы хотим разработать функцию `pow(x, n)`, которая возводит `x` в целую степень `n`, для простоты `n≥0`.
Ещё до разработки мы можем представить себе, что эта функция будет делать и описать это по методике BDD.
Это описание называется *спецификация* (или, как говорят в обиходе, "спека") и выглядит так:
@ -50,31 +49,29 @@ describe("pow", function() {
```
У спецификации есть три основных строительных блока, которые вы видите в примере выше:
<dl>
<dt>`describe(название, function() { ... })`</dt>
<dd>Задаёт, что именно мы описываем, используется для группировки "рабочих лошадок" -- блоков `it`. В данном случае мы описываем функцию `pow`.</dd>
<dt>`it(название, function() { ... })`</dt>
<dd>В названии блока `it` *человеческим языком* описывается, что должна делать функция, далее следует *тест*, который проверяет это.</dd>
<dt>`assert.equal(value1, value2)`</dt>
<dd>Код внутри `it`, если реализация верна, должен выполняться без ошибок.
Различные функции вида `assert.*` используются, чтобы проверить, делает ли `pow` то, что задумано. Пока что нас интересует только одна из них -- `assert.equal`, она сравнивает свой первый аргумент со вторым и выдаёт ошибку в случае, когда они не равны. В данном случае она проверяет, что результат `pow(2, 3)` равен `8`.
`describe(название, function() { ... })`
: Задаёт, что именно мы описываем, используется для группировки "рабочих лошадок" -- блоков `it`. В данном случае мы описываем функцию `pow`.
`it(название, function() { ... })`
: В названии блока `it` *человеческим языком* описывается, что должна делать функция, далее следует *тест*, который проверяет это.
Есть и другие виды сравнений и проверок, которые мы увидим далее.</dd>
</dl>
`assert.equal(value1, value2)`
: Код внутри `it`, если реализация верна, должен выполняться без ошибок.
Различные функции вида `assert.*` используются, чтобы проверить, делает ли `pow` то, что задумано. Пока что нас интересует только одна из них -- `assert.equal`, она сравнивает свой первый аргумент со вторым и выдаёт ошибку в случае, когда они не равны. В данном случае она проверяет, что результат `pow(2, 3)` равен `8`.
Есть и другие виды сравнений и проверок, которые мы увидим далее.
## Поток разработки
Как правило, поток разработки таков:
<ol>
<li>Пишется спецификация, которая описывает самый базовый функционал.</li>
<li>Делается начальная реализация.</li>
<li>Для проверки соответствия спецификации мы задействуем одновременно фреймворк, в нашем случае [Mocha](http://mochajs.org/) вместе со спецификацией и реализацией. Фреймворк запускает все тесты `it` и выводит ошибки, если они возникнут. При ошибках вносятся исправления.</li>
<li>Спецификация расширяется, в неё добавляются возможности, которые пока, возможно, не поддерживаются реализацией.</li>
<li>Идём на пункт 3, делаем реализацию, и так далее, до победного конца.</li>
</ol>
1. Пишется спецификация, которая описывает самый базовый функционал.
2. Делается начальная реализация.
3. Для проверки соответствия спецификации мы задействуем одновременно фреймворк, в нашем случае [Mocha](http://mochajs.org/) вместе со спецификацией и реализацией. Фреймворк запускает все тесты `it` и выводит ошибки, если они возникнут. При ошибках вносятся исправления.
4. Спецификация расширяется, в неё добавляются возможности, которые пока, возможно, не поддерживаются реализацией.
5. Идём на пункт 3, делаем реализацию, и так далее, до победного конца.
Разработка ведётся *итеративно*, один проход за другим, пока спецификация и реализация не будут завершены.
@ -82,34 +79,30 @@ describe("pow", function() {
## Пример в действии
Для запуска тестов нужны соответствующие JavaScript-библиотеки.
Для запуска тестов нужны соответствующие JavaScript-библиотеки.
Мы будем использовать:
<ul>
<li>[Mocha](http://mochajs.org/) -- эта библиотека содержит общие функции для тестирования, включая `describe` и `it`.</li>
<li>[Chai](http://chaijs.com) -- библиотека поддерживает разнообразные функции для проверок. Есть разные "стили" проверки результатов, с которыми мы познакомимся позже, на текущий момент мы будем использовать лишь `assert.equal`.</li>
<li>[Sinon](http://sinonjs.org/) -- для эмуляции и хитрой подмены функций "заглушками", понадобится позднее.</li>
</ul>
- [Mocha](http://mochajs.org/) -- эта библиотека содержит общие функции для тестирования, включая `describe` и `it`.
- [Chai](http://chaijs.com) -- библиотека поддерживает разнообразные функции для проверок. Есть разные "стили" проверки результатов, с которыми мы познакомимся позже, на текущий момент мы будем использовать лишь `assert.equal`.
- [Sinon](http://sinonjs.org/) -- для эмуляции и хитрой подмены функций "заглушками", понадобится позднее.
Эти библиотеки позволяют тестировать JS не только в браузере, но и на сервере Node.JS. Здесь мы рассмотрим браузерный вариант, серверный использует те же функции.
Пример HTML-страницы для тестов:
```html
<!--+ src="index.html" -->
```
[html src="index.html"]
Эту страницу можно условно разделить на четыре части:
<ol>
<li>Блок `<head>` -- в нём мы подключаем библиотеки и стили для тестирования, нашего кода там нет.</li>
<li>Блок `<script>` с реализацией спецификации, в нашем случае -- с кодом для `pow`.</li>
<li>Далее подключаются тесты, файл `test.js` содержит `describe("pow", ...)`, который был описан выше. Методы `describe` и `it` принадлежат библиотеке Mocha, так что важно, что она была подключена выше.</li>
<li>Элемент `<div id="mocha">` будет использоваться библиотекой Mocha для вывода результатов. Запуск тестов инициируется командой `mocha.run()`.</li>
</ol>
1. Блок `<head>` -- в нём мы подключаем библиотеки и стили для тестирования, нашего кода там нет.
2. Блок `<script>` с реализацией спецификации, в нашем случае -- с кодом для `pow`.
3. Далее подключаются тесты, файл `test.js` содержит `describe("pow", ...)`, который был описан выше. Методы `describe` и `it` принадлежат библиотеке Mocha, так что важно, что она была подключена выше.
4. Элемент `<div id="mocha">` будет использоваться библиотекой Mocha для вывода результатов. Запуск тестов инициируется командой `mocha.run()`.
Результат срабатывания:
[iframe height=250 src="pow-1" border=1 edit]
[iframe height=250 src="pow-1" border=1 edit]
Пока что тесты не проходят, но это логично -- вместо функции стоит "заглушка", пустой код.
@ -127,11 +120,11 @@ function pow() {
О, вот теперь работает:
[iframe height=250 src="pow-min" border=1 edit]
[iframe height=250 src="pow-min" border=1 edit]
## Исправление спецификации
Функция, конечно, ещё не готова, но тесты проходят. Это ненадолго :)
Функция, конечно, ещё не готова, но тесты проходят. Это ненадолго :)
Здесь мы видим ситуацию, которая (и не обязательно при ленивом программисте!) бывает на практике -- да, есть тесты, они проходят, но увы, функция работает неправильно.
@ -139,45 +132,39 @@ function pow() {
В первую очередь не реализация исправляется, а уточняется спецификация, пишется (падающий) тест.
Сейчас мы расширим спецификацию, добавив проверку на `pow(3, 4) = 81`.
Сейчас мы расширим спецификацию, добавив проверку на `pow(3, 4) = 81`.
Здесь есть два варианта организации кода:
<ol>
<li>Первый вариант -- добавить `assert` в тот же `it`:
1. Первый вариант -- добавить `assert` в тот же `it`:
```js
describe("pow", function() {
```js
describe("pow", function() {
it("возводит в n-ю степень", function() {
assert.equal(pow(2, 3), 8);
*!*
assert.equal(pow(3, 4), 81);
*/!*
});
it("возводит в n-ю степень", function() {
assert.equal(pow(2, 3), 8);
*!*
assert.equal(pow(3, 4), 81);
*/!*
});
});
```
});
```
2. Второй вариант -- сделать два теста:
</li>
<li>Второй вариант -- сделать два теста:
```js
describe("pow", function() {
```js
describe("pow", function() {
it("при возведении 2 в 3ю степень результат 8", function() {
assert.equal(pow(2, 3), 8);
});
it("при возведении 2 в 3ю степень результат 8", function() {
assert.equal(pow(2, 3), 8);
});
it("при возведении 3 в 4ю степень равен 81", function() {
assert.equal(pow(3, 4), 81);
});
it("при возведении 3 в 4ю степень равен 81", function() {
assert.equal(pow(3, 4), 81);
});
});
```
</li>
</ol>
});
```
Их принципиальное различие в том, что если `assert` обнаруживает ошибку, то он тут же прекращает выполнение блоки `it`. Поэтому в первом варианте, если вдруг первый `assert` "провалился", то про результат второго мы никогда не узнаем.
@ -187,11 +174,12 @@ describe("pow", function() {
**Один тест тестирует ровно одну вещь.**
Если мы явно видим, что тест включает в себя совершенно независимые проверки -- лучше разбить его на два более простых и наглядных.
Если мы явно видим, что тест включает в себя совершенно независимые проверки -- лучше разбить его на два более простых и наглядных.
По этим причинам второй вариант здесь предпочтительнее.
По этим причинам второй вариант здесь предпочтительнее.
Результат:
[iframe height=250 src="pow-2" edit border="1"]
Как и следовало ожидать, второй тест не проходит. Ещё бы, ведь функция всё время возвращает `8`.
@ -232,8 +220,8 @@ describe("pow", function() {
```
Результат:
[iframe height=250 src="pow-3" edit border="1"]
[iframe height=250 src="pow-3" edit border="1"]
## Вложенный describe
@ -273,13 +261,12 @@ describe("pow", function() {
В будущем мы сможем в добавить другие тесты `it` и блоки `describe` со своими вспомогательными функциями.
[smart header="before/after и beforeEach/afterEach"]
В каждом блоке `describe` можно также задать функции `before/after`, которые будут выполнены до/после запуска тестов, а также `beforeEach/afterEach`, которые выполняются до/после каждого `it`.
````smart header="before/after и beforeEach/afterEach"
В каждом блоке `describe` можно также задать функции `before/after`, которые будут выполнены до/после запуска тестов, а также `beforeEach/afterEach`, которые выполняются до/после каждого `it`.
Например:
```js
//+ no-beautify
```js no-beautify
describe("Тест", function() {
before(function() { alert("Начало тестов"); });
@ -290,7 +277,7 @@ describe("Тест", function() {
it('тест 1', function() { alert('1'); });
it('тест 2', function() { alert('2'); });
});
```
@ -300,17 +287,17 @@ describe("Тест", function() {
Начало тестов
Вход в тест
1
Выход из теста
Выход из теста
Вход в тест
2
2
Выход из теста
Конец тестов
```
[edit src="beforeafter"]Открыть пример с тестами в песочнице[/edit]
[edit src="beforeafter" title="Открыть пример с тестами в песочнице"]
Как правило, `beforeEach/afterEach` (`before/each`) используют, если необходимо произвести инициализацию, обнулить счётчики или сделать что-то ещё в таком духе между тестами (или их группами).
[/smart]
Как правило, `beforeEach/afterEach` (`before/each`) используют, если необходимо произвести инициализацию, обнулить счётчики или сделать что-то ещё в таком духе между тестами (или их группами).
````
## Расширение спецификации
@ -343,6 +330,7 @@ describe("pow", function() {
```
Результат с новыми тестами:
[iframe height=450 src="pow-nan" edit border="1"]
Конечно, новые тесты не проходят, так как наша реализация ещё не поддерживает их. Так и задумано: сначала написали заведомо не работающие тесты, а затем пишем реализацию под них.
@ -353,9 +341,9 @@ describe("pow", function() {
Она потребовалась, потому что сравнивать с `NaN` обычным способом нельзя: `NaN` не равно никакому значению, даже самому себе, поэтому `assert.equal(NaN, x)` не подойдёт.
Кстати, мы и ранее могли бы использовать `assert(value1 == value2)` вместо `assert.equal(value1, value2)`. Оба этих `assert` проверяют одно и тоже.
Кстати, мы и ранее могли бы использовать `assert(value1 == value2)` вместо `assert.equal(value1, value2)`. Оба этих `assert` проверяют одно и тоже.
Однако, между этими вызовами есть отличие в деталях сообщения об ошибке.
Однако, между этими вызовами есть отличие в деталях сообщения об ошибке.
При "упавшем" `assert` в примере выше мы видим `"Unspecified AssertionError"`, то есть просто "что-то пошло не так", а при `assert.equal(value1, value2)` -- будут дополнительные подробности: `expected 8 to equal 81`.
@ -363,15 +351,13 @@ describe("pow", function() {
Вот самые востребованные `assert`-проверки, встроенные в Chai:
<ul>
<li>`assert(value)` -- проверяет что `value` является `true` в логическом контексте.</li>
<li>`assert.equal(value1, value2)` -- проверяет равенство `value1 == value2`.</li>
<li>`assert.strictEqual(value1, value2)` -- проверяет строгое равенство `value1 === value2`.</li>
<li>`assert.notEqual`, `assert.notStrictEqual` -- проверки, обратные двум предыдущим.</li>
<li>`assert.isTrue(value)` -- проверяет, что `value === true`</li>
<li>`assert.isFalse(value)` -- проверяет, что `value === false`</li>
<li>...более полный список -- в [документации](http://chaijs.com/api/assert/)</li>
</ul>
- `assert(value)` -- проверяет что `value` является `true` в логическом контексте.
- `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`
- ...более полный список -- в [документации](http://chaijs.com/api/assert/)
В нашем случае хорошо бы иметь проверку `assert.isNaN`, но, увы, такого метода нет, поэтому приходится использовать самый общий `assert(...)`. В этом случае для того, чтобы сделать сообщение об ошибке понятнее, желательно добавить к `assert` описание.
@ -405,29 +391,25 @@ describe("pow", function() {
В коде тестов выше можно было бы добавить описание и к `assert.equal`, указав в конце: `assert.equal(value1, value2, "описание")`, но с равенством обычно и так всё понятно, поэтому мы так делать не будем.
## Итого
Итак, разработка завершена, мы получили полноценную спецификацию и код, который её реализует.
Задачи выше позволяют дополнить её, и в результате может получиться что-то в таком духе:
```js
//+ src="pow-full/test.js"
```
[js src="pow-full/test.js"]
[edit src="pow-full"]Открыть полный пример с реализацией в песочнице[/edit]
[edit src="pow-full" title="Открыть полный пример с реализацией в песочнице"]
Эту спецификацию можно использовать как:
<ol>
<li>**Тесты**, которые гарантируют правильность работы кода.</li>
<li>**Документацию** по функции, что она конкретно делает.</li>
<li>**Примеры** использования функции, которые демонстрируют её работу внутри `it`.</li>
</ol>
1. **Тесты**, которые гарантируют правильность работы кода.
2. **Документацию** по функции, что она конкретно делает.
3. **Примеры** использования функции, которые демонстрируют её работу внутри `it`.
Имея спецификацию, мы можем улучшать, менять, переписывать функцию и легко контролировать её работу, просматривая тесты.
Особенно важно это в больших проектах.
Особенно важно это в больших проектах.
Бывает так, что изменение в одной части кода может повлечь за собой "падение" другой части, которая её использует. Так как всё-всё в большом проекте руками не перепроверишь, то такие ошибки имеют большой шанс остаться в продукте и вылезти позже, когда проект увидит посетитель или заказчик.
@ -447,6 +429,6 @@ describe("pow", function() {
## Что дальше?
В дальнейшем условия ряда задач будут уже содержать в себе тесты. На них вы познакомитесь с дополнительными примерами.
В дальнейшем условия ряда задач будут уже содержать в себе тесты. На них вы познакомитесь с дополнительными примерами.
Как правило, они будут вполне понятны, даже если немного выходят за пределы этой главы.