Статьи

Лучшее тестирование CoffeeScript с Mocha

В последнее время я много работал над CoffeeScript . Одной из проблем, с которой я столкнулся на раннем этапе, было тестирование: я не хотел вручную конвертировать CoffeeScript в JavaScript, прежде чем я смог его протестировать. Вместо этого я хотел протестировать непосредственно из CoffeeScript. Как я в итоге сделал это? Читай дальше что бы узнать!

Переизданный учебник

Каждые несколько недель мы пересматриваем некоторые из любимых постов нашего читателя на протяжении всей истории сайта. Этот учебник был впервые опубликован в ноябре 2012 года.


Вам необходимо установить Node.js и Node Package Manager .

Прежде чем мы продолжим, я укажу, что вы должны иметь приличное знание CoffeeScript для этого урока; Я не буду объяснять кусочки здесь. Если вы заинтересованы в CoffeeScript, вам следует ознакомиться со статьями CoffeeScript, доступными здесь, на Nettuts + , или с документацией CoffeeScript .

Кроме того, для этого урока вам необходимо установить Node.js и менеджер пакетов узлов (npm). Если у вас их не установлено, не беспокойтесь: зайдите на nodejs.org и скачайте установщик для вашей платформы; тогда, ну, установите его!


Мы будем строить начало приложения списка задач (клише, я знаю). Это будут классы CoffeeScript. Затем мы напишем несколько тестов с Mocha и Chai, чтобы проверить эту функциональность.

Почему и мокко, и чай? Ну, Mocha — это среда тестирования, но она не включает в себя фактический компонент утверждений. Это может звучать странно: в конце концов, есть не так много для библиотеки тестирования, не так ли? Ну, есть, в случае Мокко. Функции, которые привели меня в библиотеку, двояки: возможность запускать тесты из командной строки (вместо HTML-страницы для их запуска в браузере) и возможность запускать тесты в CoffeeScripts без необходимости конвертировать этот код в JavaScript (как минимум вручную: Mocha делает это за кулисами). Есть и другие функции, о которых я не буду здесь говорить, в том числе:

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

И так далее. Смотрите больше на домашней странице Мокко . Для установки Mocha просто запустите npm install -g mocha , и все готово.

Что касается Chai: это отличная библиотека утверждений, которая предлагает интерфейсы для выполнения как BDD, так и TDD ; Вы можете использовать его как в браузере, так и в командной строке через узел, как мы будем использовать сегодня. Установите его для Node, через npm install -g chai .

Теперь, когда у нас установлены библиотеки, давайте начнем писать код.


Давайте начнем с настройки мини-проекта. Создайте папку проекта. Затем создайте еще две папки в этой src : src и test . Наш код CoffeeScript будет src папку src , и наши тесты будут включены, как вы уже догадались, в папку tests . Mocha ищет test папку по умолчанию, так что, сделав это, мы немного сэкономим при печати.

Мокко ищет test папку по умолчанию.

Мы собираемся создать два класса CoffeeScript: Task , который будет элементом todo, и TaskList , который будет списком элементов todo (да, это больше, чем массив). Мы поместим их обоих в файл src/task.coffee . Тогда тесты для этого будут в test/taskTest.coffee . Конечно, мы могли бы разделить их на их собственные файлы, но мы просто не собираемся делать это сегодня.

Мы должны начать с импорта библиотеки Chai и включения синтаксиса BDD. Вот как:

1
2
chai = require ‘chai’
chai.should()

chai.should метод chai.should , мы фактически добавляем свойство chai.should в Object.prototype . Это позволяет нам писать тесты, которые читаются так:

1
task.name.should.equal «some string»

Если вы предпочитаете синтаксис TDD, вы можете сделать это:

1
expect = chai.expect

… Который позволяет вам писать тесты следующим образом:

1
expect(task.name).to.equal «some string»

На самом деле, нам придется использовать оба из них, как вы увидите; однако мы будем максимально использовать синтаксис BDD.

Теперь нам нужно импортировать наши классы Task и TaskList :

1
{TaskList, List} = require ‘../src/task’

Если вы не знакомы с этим синтаксисом, это деструктурированное назначение CoffeeScript на работе, а также некоторые его объектные литералы сахара. По сути, наш вызов require возвращает объект с двумя свойствами, которые являются нашими классами. Эта строка вытягивает их из этого объекта и дает нам две переменные с именами Task и TaskList , каждая из которых указывает на соответствующий класс.


Большой! А как насчет теста? Прелесть синтаксиса Mocha в том, что его блоки ( describe и it ) идентичны синтаксису Jasmine (оба очень похожи на RSpec). Вот наш первый тест:

1
2
3
4
5
describe ‘Task instance’, ->
   task1 = task2 = null
   it ‘should have a name’, ->
       task1 = new Task ‘feed the cat’
       task1.name.should.equal ‘feed the cat’

Мы начнем с describe вызова: все эти тесты предназначены для экземпляра Test. Установив test1 = test2 = null вне наших индивидуальных тестов, мы можем использовать эти значения для нескольких тестов.

Затем в нашем первом тесте мы просто создаем задачу и проверяем, что ее свойство name имеет правильное значение. Прежде чем писать код для этого, давайте добавим еще два теста:

1
2
3
4
5
it ‘should be initially incomplete’, ->
    task1.status.should.equal ‘incomplete’
it ‘should be able to be completed’, ->
    task1.complete().should.be.true
    task1.status.should.equal ‘complete’

Хорошо, давайте запустим эти тесты, чтобы убедиться, что они не работают. Для этого откроем командную строку и перейдем в папку вашего проекта. Затем выполните эту команду:

1
mocha —compilers coffee:coffee-script

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

Если вместо того, чтобы увидеть это, вы получаете ошибку Cannot find module '../src/task' , это потому, что ваш файл src/task.coffee еще не существует. Сделайте указанный файл, и вы должны получить указанную ошибку.


Ну, теперь, когда у нас есть провальные тесты, пришло время написать код, правильно? Откройте этот файл src/task.coffee и начнем.

1
2
class Task
   constructor: (@name) ->

Только этого достаточно, чтобы пройти наш первый тест. Если вы не знакомы с синтаксисом этого параметра, он просто устанавливает любое значение, переданное new Task @name (или this.name ). Однако давайте добавим еще одну строку в этот конструктор:

1
@status = ‘incomplete’

Это хорошо. Теперь вернитесь к терминалу и повторите наши тесты. Вы найдете это — подождите секунду, ничего не изменилось! Почему наши первые два теста не пройдены?

Простая проблема, на самом деле. Поскольку компилятор CoffeeScript оборачивает код в каждом файле в IIFE (или в вызывающую себя анонимную функцию), нам нужно «экспортировать» все, что мы хотим, чтобы оно было доступно из других файлов. В браузере вы бы сделали что-то вроде window.Whatever = Whatever . Для Node вы можете использовать global или exports . Мы будем использовать exports , поскольку 1) это считается наилучшей практикой и 2) к этому мы готовились при настройке наших тестов (помните наш вызов require ?). Поэтому в конце нашего файла task.coffee добавьте следующее:

1
2
root = exports ?
root.Task = Task

Имея это в виду, вы должны обнаружить, что два из наших трех тестов теперь проходят:

Чтобы пройти последний тест, мы должны добавить complete метод. Попробуй это:

1
2
3
complete: ->
   @status = ‘complete’
   true

Теперь все тесты проходят:

Сейчас самое время упомянуть, что у Mocha есть несколько разных отчетов: это просто разные способы вывода результатов теста. Вы можете запустить mocha --reporters чтобы увидеть ваши варианты:

По умолчанию Mocha использует точечный репортер. Однако я предпочитаю репортер спецификаций, поэтому я -R spec в конце команды ( -R — флаг настройки репортера).


Давайте добавим функцию в наш класс Task : мы позволим задачам зависеть от других задач. Если «родительская» задача не выполнена, «дочерняя» задача не может быть выполнена. Мы сделаем эту функцию простой и позволим задачам иметь только одну подзадачу. Мы также не будем проверять рекурсивность, поэтому хотя можно будет установить две задачи как родительские и дочерние по отношению друг к другу, они сделают обе задачи неосуществимыми.

Сначала тесты!

01
02
03
04
05
06
07
08
09
10
11
12
it ‘should be able to be dependent on another task’, ->
    task1 = new Task ‘wash dishes’
    task2 = new Task ‘dry dishes’
     
    task2.dependsOn task1
 
    task2.status.should.equal ‘dependent’
    task2.parent.should.equal task1
    task1.child.should.equal task2
 
it ‘should refuse completion it is dependent on an uncompleted task’, ->
    (-> task2.complete()).should.throw «Dependent task ‘wash dishes’ is not completed.»

Экземпляры Task будут иметь метод dependsOn , который dependsOn задачу, которая станет их родителем. Задачи, у которых есть родительская задача, должны иметь статус «зависимые». Кроме того, обе задачи получают либо parent либо child свойство, которое указывает на соответствующий экземпляр задачи.

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

Запустите эти тесты, и вы увидите, что оба провалились. Время кодирования!

1
2
3
dependsOn: (@parent) ->
   @parent.child = @
   @status = ‘dependent’

Опять же, очень просто: мы просто устанавливаем параметр задачи для родительской задачи и присваиваем ему дочернее свойство, которое указывает на this экземпляр задачи. Затем мы устанавливаем статус this задачи как «зависимый».

Если вы запустите это сейчас, вы увидите, что один из наших тестов проходит, а второй нет: это потому, что наш метод complete не проверяет незавершенную родительскую задачу. Давайте изменим это.

1
2
3
4
5
complete: ->
   if @parent?
       throw «Dependent task ‘#{@parent.name}’ is not completed.»
   @status = ‘complete’
   true

Вот завершенный метод complete : если есть родительская задача, и она не выполнена, мы выдаем ошибку. В противном случае мы завершим задачу. Теперь все тесты должны пройти.


Далее мы TaskList класс TaskList . Опять же, мы начнем с теста:

1
2
3
4
5
6
describe ‘TaskList’, ->
   taskList = null
   it ‘should start with no tasks’, ->
       taskList = new TaskList
       taskList.tasks.length.should.equal 0
       taskList.length.should.equal 0

На данный момент это старая вещь: мы создаем объект TaskList и проверяем его tasks и свойства length чтобы убедиться, что они оба равны нулю. Как вы можете догадаться, tasks — это массив, содержащий задачи, а length — это просто удобное свойство, которое мы будем обновлять при добавлении или удалении задач; это просто избавляет нас от необходимости писать list.tasks.length .

Чтобы пройти этот тест, мы сделаем этот конструктор:

1
2
3
4
class TaskList
   constructor: () ->
       @tasks = []
       @length = 0

Хорошее начало, и это проходит наш тест.

Мы хотим иметь возможность добавлять задачи в список задач, верно? У нас будет метод add который может принимать либо экземпляр Task , либо строку, которую он преобразует в экземпляр Task .

Наши тесты:

1
2
3
4
5
6
7
8
9
it ‘should accept new tasks as tasks’, ->
    task = new Task ‘buy milk’
    taskList.add task
    taskList.tasks[0].name.should.equal ‘buy milk’
    taskList.length.should.equal 1
it ‘should accept new tasks as string’, ->
    taskList.add ‘take out garbage’
    taskList.tasks[1].name.should.equal ‘take out garbage’
    taskList.length.should.equal 2

Сначала мы добавляем фактический объект Task и проверяем массив taskList.tasks чтобы убедиться, что он был добавлен. Затем мы добавляем строку и проверяем, чтобы объект Task с правильным именем был добавлен в массив tasks . В обоих случаях мы также проверяем длину taskList , чтобы убедиться, что это свойство обновляется.

И функция:

1
2
3
4
5
6
add: (task) ->
   if typeof task is ‘string’
       @tasks.push new Task task
   else
       @tasks.push task
   @length = @tasks.length

Я думаю, довольно понятно. И теперь наши тесты проходят:

Конечно, мы можем захотеть удалить задачи из нашего списка, верно?

1
2
3
4
it ‘should remove tasks’, ->
   i = taskList.length — 1
   taskList.remove taskList.tasks[i]
   expect(taskList.tasks[i]).to.not.be.ok

Во-первых, мы вызываем метод remove (который еще не написан, конечно), передавая ему последнюю задачу в списке. Конечно, мы могли бы просто жестко закодировать индекс 1 , но я сделал это таким образом, потому что это делает этот тест гибким: если мы изменили наши предыдущие тесты или добавили больше тестов над этим, это может измениться. Конечно, мы должны удалить последнюю, потому что в противном случае задача после нее займет свое место, и в этом индексе будет что-то, когда мы ожидаем, что ничего не будет.

Говоря об ожидании, обратите внимание, что мы используем функцию expect и синтаксис здесь вместо нашего обычного. Это потому, что taskList.tasks[i] будет undefined , который не наследуется от Object.prototype , и поэтому мы не можем его использовать.

О, да, нам все еще нужно написать функцию remove :

1
2
3
4
remove: (task) ->
   i = @tasks.indexOf task
   @tasks = @tasks[0…i].concat @tasks[i+1..] if i > -1
   @length = @tasks.length

Некоторая изощренная работа с массивами в сочетании с диапазонами CoffeeScript и сокращением соединения массивов закрывает эту сделку для нас. Мы просто разделяем все элементы перед тем, как удалить, и все элементы после него; мы concat эти два массива вместе. Конечно, мы обновим @length соответственно. Можете ли вы сказать «сдача экзаменов»?

Давайте сделаем еще одну вещь. Мы хотим напечатать наш (относительно) красивый список текущих задач. Это будет наш самый сложный (или, по крайней мере, наш самый длинный) тест:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
it ‘should print out the list’, ->
        taskList = new TaskList
        task0 = new Task ‘buy milk’
        task1 = new Task ‘go to store’
        task2 = new Task ‘another task’
        task3 = new Task ‘sub-task’
        task4 = new Task ‘sub-sub-task’
 
        taskList.add task0
        taskList.add task1
        taskList.add task2
        taskList.add task3
        taskList.add task4
 
        task0.dependsOn task1
        task4.dependsOn task3
        task3.dependsOn task2
 
        task1.complete()
 
        desiredOutput = «»»Tasks
 
— buy milk (depends on ‘go to store’)
— go to store (completed)
— another task
— sub-task (depends on ‘another task’)
— sub-sub-task (depends on ‘sub-task’)
 
«»»
        taskList.print().should.equal desiredOutput

Что тут происходит? Сначала мы создаем новый объект TaskList чтобы начать с нуля. Затем мы создаем пять задач и добавляем их в taskList . Далее мы установили несколько зависимостей. Наконец мы выполнили одну из наших задач.

Мы используем синтаксис heredoc CoffeeScript для создания многострочной строки. Как видите, у нас все довольно просто. Если у задачи есть родительская задача, она упоминается в скобках после имени задачи. Если задание выполнено, мы тоже это ставим.

Готов написать функцию?

1
2
3
4
5
6
7
8
print: ->
   str = «Tasks\n\n»
   for task in @tasks
       str += «- #{task.name}»
       str += » (depends on ‘#{task.parent.name}’)» if task.parent?
       str += ‘ (complete)’ if task.status is ‘complete’
       str += «\n»
   str

Это на самом деле довольно просто: мы просто просматриваем массив @tasks и добавляем их в строку. Если у них есть родитель, мы добавляем это, и если они полны, мы также добавляем это. Обратите внимание, что мы используем форму модификатора оператора if , чтобы ужесточить наш код. Затем мы возвращаем строку.

Теперь все наши тесты должны пройти:


Попробуйте добавить несколько функций, чтобы освоить все это.

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

  • Не допускайте, чтобы экземпляры Task могли зависеть друг от друга (рекурсивные зависимости).
  • Заставьте метод TaskList::add выдавать ошибку, если он получает что-то отличное от строки или объекта Task .

В эти дни я нахожу CoffeeScript все более привлекательным, но самым большим его недостатком является то, что он должен быть скомпилирован в JavaScript, прежде чем он будет полезен. Я благодарен за все, что сводит на нет некоторые из этих прерывателей рабочего процесса, и Мокко определенно делает это. Конечно, он не идеален (так как он компилируется в JS перед выполнением кода, номера строк в ошибках не совпадают с номерами ваших строк CoffeeScript), но для меня это шаг в правильном направлении!

Как насчет тебя? Если вы используете CoffeeScript, как вы делали тестирование? Дай мне знать в комментариях.