Unlimited Plugins, WordPress themes, videos & courses! Unlimited asset downloads! From $16.50/m
Advertisement
  1. Code
  2. Tools & Tips
Code

Тестирование в Node.js

by
Difficulty:BeginnerLength:LongLanguages:

Russian (Pусский) translation by Ilya Nikov (you can also view the original English article)

Цикл разработки, основанный на тестах, упрощает мыслительный процесс написания кода, делает его легким и ускоряет в долгосрочной перспективе. Но само по себе простое написание тестов еще не является достаточным, необходимо знать, какие типы тестов писать и как структурировать код. В этой статье мы рассмотрим создание небольшого приложения на Node.js следуя шаблону TDD.

Помимо простых «unit» тестов, с которыми мы все знакомы; Мы также можем написать Node.js тесты Когда выполняется асинхронный код, это добавляет дополнительное измерение, которое заключается в том, что мы не всегда знаем порядок выполнения функций, или мы можем пытаться что-то проверить в обратном вызове или проверить, как работает асинхронная функция.

В этой статье мы будем создавать приложение Node, которое может искать файлы, соответствующие данному запросу. Я знаю, что для этого уже есть инструменты (ack), но для демонстрации TDD я думаю, что это может как раз подходящим проектом.

Первый шаг, очевидно, состоит в том, чтобы написать некоторые тесты, но еще до этого нам нужно выбрать фреймворк для тестирования. Вы можете использовать vanilla Node, так как есть встроенная библиотека assert, но этого может быть недостаточно, так как у нас нет test runner для тестов.

Другой вариант и, вероятно, мой любимый для общего использования - Jasmine. Он является довольно самодостаточным, и у вас не будет других зависимостей, которые надо будет добавить к уже имеющимся скриптам. Его синтаксис очень чист и прост в чтении. Единственная причина, по которой я не буду использовать его сегодня, состоит в том, что я думаю, что Джек Франклин отлично справился с этим в своей недавней серии Tuts +, и рассказал про все возможные варианты, чтобы вы могли выбрать лучший инструмент для своей ситуации.


Что мы будем строить

В этой статье мы будем использовать гибкий тестовый runner «Mocha» вместе с библиотекой утверждений Chai.

В отличие от Jasmine  который больше похож на весь набор тестов в одном пакете, Mocha только заботится об общей структуре, но не имеет никакого отношения к фактическим утверждениям. Это позволяет сохранять постоянный внешний вид при выполнении ваших тестов, но также позволяет использовать ту библиотеку утверждений, которая лучше всего подходит для вашей ситуации.

Например, если вы собираетесь использовать библиотеку «assert», вы можете связать ее с Mocha, чтобы добавить некоторую структуру к вашим тестам.

Chai - довольно популярный вариант. Даже без каких-либо плагинов, используя API по умолчанию, у вас есть три разных синтаксиса, которые вы можете использовать в зависимости от того, хотите ли вы использовать более классический стиль TDD или более подробный синтаксис BDD.

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


Настройка

Чтобы начать работу, давайте установим Mocha глобально, запустив:

Когда эта команда завершится, создайте новую папку для нашего проекта и запустите внутри нее следующее:

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

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


Структурирование вашего приложения

Следуя подходу TDD, важно знать, что должно иметь свои тесты, а что - нет. Эмпирическое правило заключается в том, чтобы не писать тесты для других людей, которые уже тестировали код. Я имею в виду следующее: предположим, что ваш код открывает файл, вам не нужно проверять отдельную функцию fs, она является частью языка и, предположительно, уже хорошо протестирована. То же самое происходит при использовании сторонних библиотек, вы не должны структурировать функции, которые в первую очередь вызывают эти типы функций. Вы на самом деле не пишете тесты для них, и из-за этого у вас могут возникнуть пробелы в цикле TDD.

Теперь, конечно, с каждым стилем программирования есть много разных мнений, и у людей будут разные взгляды на то, что такое TDD. Но подход, который я использую, заключается в том, что вы создаете отдельные компоненты для использования в своем приложении, каждый из которых решает уникальную функциональную проблему. Эти компоненты создаются с использованием TDD, гарантируя, что они работают должным образом, и вы не нарушите их API. Затем вы пишете свой основной скрипт, который по сути является клеем всего остального кода и не нуждается в тестировании / не может быть проверен в определенных ситуациях.

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

Следуя тому, что я только что сказал, обычной практикой является создание папки с именем «lib», куда вы помещаете все отдельные компоненты. Таким образом, к этому моменту вы уже должны установить Mocha и Chai, а затем каталог проекта с двумя папками: «lib» и «test».


Начало работы с TDD

На всякий случай, если вы новичок в TDD, я подумал, что было бы неплохо быстро осветить процесс. Основное правило заключается в том, что вы не можете написать какой-либо код, пока вам об этом не сообщат тесты.

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

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

Опять же, согласно нашему правилу выше, если тест проходит сразу, вы не можете написать какой-либо код, потому что тест не сказал вам об этом. Когда вы постоянно сначала пишете тесты, а затем реализуете функции, то вы тем самым создаете надежные  модули, на которые вы можете положиться.

После того, как вы закончите внедрение и тестирование своего компонента, вы можете вернуться и реорганизовать код, чтобы его оптимизировать и очистить, но убедитесь, что рефакторинг не прервет ни одного из проведенных вами тестов и, что более важно, вы не добавили никаких новых непротестированных функций.

Каждая тестовая библиотека будет иметь свой собственный синтаксис, но обычно они следуют одной и той же схеме создания утверждений, а затем проверяют, проходят ли они. Поскольку мы используем Mocha и Chai, давайте рассмотрим оба их синтаксиса, начиная с Chai.


Мокка и Чай

Я буду использовать синтаксис «expect» BDD, потому что, как я уже говорил, у Chai есть несколько вариантов из коробки. Этот синтаксис работает следующим образом: вы начинаете с вызова функции ожидания, передавая ей объект, на который хотите включить утверждение, и затем связываете его с определенным тестом. Пример того, что я имею в виду, может быть следующим:

Это основной синтаксис, мы говорим, ожидаем, что сумма 4 и 5 будет равно 9. Теперь это не самый удачный тест, потому что 4 и 5 будут сложены Node.js до того, как функция будет даже вызвана, поэтому мы по существу тестируем мои математические навыки, но я надеюсь, что вы получите общую идею. Другая вещь, которую вы должны отметить, заключается в том, что этот синтаксис не очень читабельен с точки зрения обычного английского предложения. Зная это, Chai добавил следующие chain геттеры, которые ничего не делают, но вы можете добавить их, чтобы сделать код более подробным и удобочитаемым. Chain геттеры следующие:

  • to
  • be
  • been
  • is
  • that
  • and
  • have
  • with
  • at
  • of
  • same
  • a
  • an

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

Мне очень нравится ощущение всей библиотеки, которую вы можете проверить в их API. Простые вещи, такие как отрицание операции, так же просто написать, добавив .not до теста:

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

Последнее, что я хотел бы рассмотреть, прежде чем мы приступим к нашему первому тесту, - это то, как мы структурируем наш код в Mocha

Mocha

Mocha - тестовый бегун, поэтому он на самом деле не слишком много внимания уделяет реальным тестам,  он больше заботится о структуре тестов, потому что это то, как он понимает, что происходит, и как показывать результатов. При написании тестов, вы создаете несколько describe блоков, которые описывают различные компоненты вашей библиотеки, а затем добавляете блоки it, чтобы указать конкретный тест.

Для быстрого примера предположим, что у нас есть класс JSON, и у этого класса была функция для разбора JSON, и мы хотели убедиться, что функция синтаксического анализа может обнаружить сильно форматированную строку JSON, мы могли бы структурировать ее так:

Это не сложно, и это около 80% личных предпочтений, но если вы сохраните такой формат, результаты теста должны появиться в очень читаемом формате.

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

Чтобы позаботиться обо всем этом, мы создадим модуль, который принимает строку команды и анализирует все включенные опции вместе с их значениями.

Модуль тегов

Это отличный пример модуля, который можно повторно использовать во всех приложениях с командной строкой, так как эта проблема возникает довольно часто. Это будет упрощенная версия фактического пакета, который я использую для npm под названием ClTags. Итак, для начала создайте файл с именем tags.js внутри папки lib, а затем другой файл с именем tagsSpec.js внутри папки с тестами.

Нам нужно задействовать expect функцию Chai, так как это будет синтаксис утверждения, который мы будем использовать, и нам нужно вытащить фактический файл тегов, чтобы мы могли его протестировать. В целом с некоторой начальной настройкой он должен выглядеть примерно так:

Если теперь вы запускаете команду «mocha» из корня нашего проекта, все должно проходить как ожидалось. Теперь давайте подумаем о том, что сделает наш модуль; мы хотим передать ему массив аргументов команды, который использовался для запуска приложения, а затем мы хотим, чтобы он создавал объект со всеми тегами, и было бы неплохо, если бы мы могли также передать ему объект настроек по умолчанию, поэтому, если ничего не будет перезаписано, то у нас будут уже сохранены некоторые настройки.

При работе с тегами многие приложения также предоставляют ярлыки, которые являются всего лишь одним символом, поэтому, скажем, мы хотели установить глубину нашего поиска, мы могли бы позволить пользователю либо указать что-то вроде --depth=2 или что-то вроде -d=2, что должно иметь тот же эффект.

Итак, давайте начнем с длинно сформированных тегов (например, '--depth = 2'). Для начала давайте напишем первый тест:

Мы добавили один метод к нашему набору тестов, который называется parse, и мы добавили тест для длинно сформированных тегов. Внутри этого теста я создал команду примера и добавил два утверждения для двух свойств, которые он должен получить.

Запустив Mocha, вы должны получить одну ошибку, а именно, что tags не имеют функции parse. Поэтому, чтобы исправить эту ошибку, добавим функцию parse в модуль тегов. Достаточно типичный способ создания node-модуля выглядит так:

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

Теперь давайте снова запустим Mocha, на этот раз мы должны получить ошибку, сообщающую нам, что тест не может прочитать свойство с именем depth из неопределенной переменной. Это потому, что в настоящее время наша функция parse ничего не возвращает, поэтому давайте добавим некоторый код, чтобы она возвращала объект:

Мы медленно продвигаемся вперед, если вы снова запустите Mocha, не будет выброшено никаких исключений, просто чистое сообщение об ошибке, в котором говорится, что наш пустой объект не имеет свойства depth.

No depth property

Теперь мы можем написать какой-то реальный код. Чтобы наша функция анализировала тег и добавляла его к нашему объекту, нам нужно циклически перебирать массив аргументов и удалять двойные штрихи в начале ключа.

Этот код циклически проходит через список аргументов, поэтому мы имеем дело с длинным сформированным тегом, а затем разбиваем его на первый символ равенства, чтобы создать пару ключей и значений для объекта options.

Теперь это почти решает нашу проблему, но если мы снова запустим Mocha, вы увидите, что теперь у нас есть ключ для глубины, но он установлен как строка вместо числа. Следующий фрагмент кода, который нам нужно добавить, - это преобразовать значения в числа, там где это возможно. Это может быть достигнуто с помощью некоторых функций RegEx и parseInt следующим образом:

Запустив Mocha, вы должны получить один успешно выполненный тест. Преобразование числа, возможно, должно быть в собственном тесте или, по крайней мере, упомянуто в декларации тестов, чтобы вы, по ошибке, не удалили утверждение о преобразовании числа; поэтому просто добавьте «добавить и преобразовать числа» в it объявление для этого теста или разделите его на новый блок it. Это действительно зависит от того, считаете ли вы это «очевидным поведением по умолчанию» или отдельной функцией.

First Pass

Теперь, как я пытался подчеркнуть во всей этой статье, когда вы видите успешно проходящую спецификацию, пришло время написать больше тестов. Следующее, что я хотел добавить, - это массив по умолчанию, поэтому внутри файла tagsSpec добавим следующий блок it сразу после предыдущего:

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

Запустив Mocha сейчас, вы должны получить своего рода diff, содержащий различия между ожидаемым и тем, что он получил.

Defaults Diff

Давайте теперь вернемся к модулю tags.js и добавим этот функционал. Это довольно простое исправление, нам просто нужно принять второй параметр, и когда он установлен в объект, мы можем заменить стандартный пустой объект в начале этого объекта:

Это вернет нас к зеленому состоянию. Следующее, что я хочу добавить, это возможность просто указать тег без значения и позволить ему работать как логическое значение. Например, если мы просто установим --searchContents или что-то вроде этого, он просто добавит это в наш массив параметров со значением true.

Тест для этого будет выглядеть примерно так:

Выполнение этого приведет к следующей ошибке:

Boolean Tags

Внутри цикла for, когда мы получили соответствие для длинного сформированного тега, мы проверили, содержит ли он знак равенства; мы можем быстро написать код для этого теста, добавив предложение else к этому оператору if и просто установив значение в true:

Следующее, что я хочу добавить, это замены для коротких тегов. Это будет третий параметр функции parse и он будет в основном объектом с буквами и соответствующими заменами. Вот спецификация для этого добавления:

Проблема с сокращенными тегами заключается в том, что они могут быть объединены в ряд. То, что я имею в виду под этим, в отличие от длинно сформированных тегов, где каждый из них раздельный, с короткими метками все по-другому - поскольку каждый из них имеет только длину в один символ, вы можете указать три разных, набрав -vgh. Это делает анализ более сложным, потому что нам все равно нужно разрешить оператору equals добавлять значение к указанному последнему тегу, в то же время вам нужно еще зарегистрировать другие теги. Но не беспокойтесь, все это вполне реализуемо.

Вот полное исправление с начала функции parse:

Здесь очень много кода (в сравнении), но все, что мы делаем, это разделение аргумента на знак равенства, а затем разделение этого ключа на отдельные буквы. Например, если мы передали -gj=asd, мы разделили бы asd на переменную с именем value, а затем разделили бы gj на отдельные символы. Последний символ (j в нашем примере) станет ключом для значения (asd), тогда как любые другие буквы перед ним будут добавлены в виде обычных булевых тегов. Я не хотел просто обрабатывать эти теги сейчас, на всякий случай, когда мы изменим реализацию чуть позже. Итак, мы просто конвертируем эти короткие теги в длинную форматированную версию, а затем позволяем нашему скрипту обрабатывать ее позже.

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

Теперь есть еще несколько вещей, которые мы можем добавить к этому модулю тегов, чтобы сделать его ближе к пакету npm, например, возможность хранить текстовые аргументы для таких вещей, как команды или возможность собирать весь текст в конце, как свойство запроса. Но эта статья уже итак становится слишком длинной, и я хотел бы перейти к реализации функции поиска.


Модуль поиска

Мы просто прошли через создание модуля шаг за шагом, следуя подходу TDD, и я надеюсь, что вы теперь понимаете как ему следовать. Но ради целей этой статьи, в остальной ее части, я ускорю процесс тестирования, объединив все вместе и покажу вам окончательные версии тестов. Это скорее руководство к различным ситуациям, которые могут возникнуть и как писать тесты для них.

Поэтому просто создайте файл с именем search.js внутри папки lib и файл searchSpec.js внутри тестовой папки.

Затем откройте файл spec и давайте настроим наш первый тест, который может быть для функции, чтобы получить список файлов на основе параметра depth, это также отличный пример для тестов, для которых требуется немного внешней настройки для их работы. Когда вы работаете с внешними объектно-подобными данными или в нашем случае с файлами, вы должно быть захотите иметь предопределенную настройку, которая, будет работать с вашими тестами, но вы также не захотите добавлять фейковые данные в свою систему.

Существует два варианта решения этой проблемы: вы можете либо замокать эти данные, как я уже упоминал выше, или если вы имеете дело с языками собственных команд для загрузки данных, вам не обязательно их проверять. В подобных случаях вы можете просто предоставить «извлеченные» данные и продолжить тестирование, так как мы сделали с командной строкой в библиотеке тегов. Но в этом случае мы тестируем рекурсивную функциональность, которую мы добавляем к возможностям чтения файлов, в зависимости от указанной глубины. В таких случаях вам нужно написать тест, поэтому нам нужно создать несколько демонстрационных файлов для проверки чтения файла. Альтернатива заключается в том, чтобы, возможно, сделать заглушки для функции fs только для запуска, которые на самом деле ничего не делают, и тогда мы можем подсчитать, сколько раз выполнялась наша фальшивая функция или что-то в этом роде (проверьте шпионов), но для нашего примера я просто собираюсь создать некоторые файлы.

Mocha предоставляет функции, которые могут запускаться как до, так и после ваших тестов, поэтому вы можете выполнять эти внешние настройки и очистку для своих тестов.

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

Они будут вызваны на основе блока describe, в котором они находятся, и вы даже можете запускать код до и после каждого it блока, используя beforeEach или afterEach. Сами функции просто используют стандартные команды node для создания и удаления файлов соответственно. Затем нам нужно написать фактический тест. Он должен сразу после функции after, все еще внутри блока describe:

Это наш первый пример тестирования функции async, но, как вы видите, это так же просто, как и раньше; все, что нам нужно сделать, это использовать функцию done, которую Mocha предоставляет в it декларациях, чтобы рассказать о том, когда мы закончим этот тест.

Mocha автоматически определит, указали ли вы done переменную в обратном вызове, и она будет ожидать, что ее вызовут, что позволит вам легко протестировать асинхронный код. Кроме того, стоит упомянуть, что этот шаблон доступен во всем Mocha, вы можете, например, использовать его в функциях before или after, если вам нужно настроить что-то асинхронно.

Затем я хотел бы написать тест, который гарантирует, что параметр глубины работает, если он установлен:

Здесь ничего особенного, просто еще один простой тест. Запустив это в Mocha, вы получите сообщение об ошибке, что поиск не имеет каких-либо методов, в основном потому, что в нем ничего не написано. Итак, давайте определимся с функцией:

Если вы снова запустите Mocha, он приостанавливает ожидание возврата этой функции async, но поскольку мы вообще не вызываем обратный вызов, тест будет просто таймаутом. По умолчанию он должен истечь через две секунды, но вы можете настроить его с помощью this.timeout (milliseconds) внутри блока describe или it блока, чтобы соответственно настроить их таймауты.

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

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

Поэтому, чтобы заставить его работать, вам нужно создать своего рода стек, в котором вы можете асинхронно обрабатывать по одному (или все сразу, если вы используете очередь вместо этого), а затем сохранить порядок таким образом. Это очень специфический алгоритм, поэтому я просто приведу фрагмент Кристофера Джеффри, который вы можете найти на Stack Overflow. Это не относится только к загрузке файлов, но я использовал это в ряде приложений, в основном, там где нужно обрабатывать массив объектов по одному с помощью асинхронных функций.

Нам нужно немного изменить его, потому что мы хотели бы иметь параметр глубины, как работает опция глубины, вы устанавливаете количество уровней папок, которые хотите проверить, или ноль, углубляться бесконечно.

Вот завершенная с помощью фрагмента функция:

Mocha должен пройти оба теста. Последняя функция, которую нам нужно реализовать, - это та, которая примет массив путей и ключевое слово поиска и вернет все совпадения. Вот тест на это:

И последнее, но не менее важное: добавим функцию в search.js:

Просто чтобы убедиться, снова запустите Mocha, вы должны пройти в общей сложности семь тестов.

All Green

Все вместе

Последний шаг - действительно написать код-клей, который объединяет все наши модули; поэтому в корне нашего проекта добавьте файл с именем app.js или что-то в этом роде и добавьте в него следующее:

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

Теперь вы можете сделать свой скрипт исполняемым (chmod + x app.js в системе Unix), а затем запустить его так:

При желании можно настроить некоторые из других заполнителей.

Action Still

Вывод

В этой статье мы создали полноценное приложение для поиска файлов, хотя и простое, но я думаю, что оно демонстрирует процесс в целом довольно хорошо.

Если вы собираетесь часто использовать TDD, то настройте свою среду. Много времени, которое люди связывают с TDD, связано с тем, что им приходится постоянно переключаться между окнами, открывать и закрывать разные файлы, а затем запускать тесты и повторять это 80 десятков раз в день. В таком случае это прерывает ваш рабочий процесс, снижая производительность. Но если у вас есть настройка редактора, например, у вас есть тесты и код бок о бок, или ваша среда IDE поддерживает прыжки назад и вперед, это экономит массу времени. Вы также можете получить свои тесты для автоматического запуска, вызвав его с помощью тега -w, чтобы следить за изменениями в файлах и автоматически запускать все тесты. Такие вещи делают весь процесс более плавным и более полезным.

Надеюсь, вам понравилась эта статья, если у вас есть вопросы, вы можете оставить их ниже, свяжитесь со мной в Twitter @gabrielmanricks или на канале Nettuts + IRC (#nettuts on freenode).

Advertisement
Advertisement
Advertisement
Advertisement
Looking for something to help kick start your next project?
Envato Market has a range of items for sale to help get you started.