One place for hosting & domains

      модуля

      Создание веб-сервера в Node.js с помощью модуля HTTP


      Автор выбрал COVID-19 Relief Fund для получения пожертвования в рамках программы Write for DOnations.

      Введение

      При просмотре веб-страницы в браузере мы отправляем запрос на другой компьютер в Интернете, который отправляет в ответ веб-страницу. Компьютер, с которым вы взаимодействуете через Интернет, называется веб-сервером. Веб-сервер получает запросы HTTP от клиентов, в том числе от вашего браузера, и отправляет им ответы HTTP, например страницы HTML или код JSON из API.

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

      Node.js позволяет разработчикам использовать JavaScript для создания серверного кода, хотя традиционно этот язык использовался в браузере для создания клиентского кода. Объединение клиентского и серверного кода в одной среде разработки упрощает создание веб-серверов, и именно поэтому Node.js стал популярным инструментом для написания серверного кода.

      В этом обучающем руководстве мы научимся создавать веб-серверы с помощью модуля http, входящего в состав Node.js. Мы создадим веб-серверы, которые смогут возвращать данные JSON, файлы CSV и веб-страницы HTML.

      Предварительные требования

      • Убедитесь, что на используемом для разработки компьютере установлен Node.js. Для целей этого обучающего руководства мы используем версию Node.js 10.19.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04.
      • Платформа Node.js позволяет создавать готовые веб-серверы. Для начала вам следует познакомиться с основами Node.js. Для этого вы можете воспользоваться нашим руководством Написание и запуск первой программы на Node.js.
      • Также мы посвятим один из разделов этого обучающего руководства асинхронному программированию. Если вы незнакомы с асинхронным программированием в Node.js или с модулем fs для взаимодействия с файлами, вы можете узнать о них больше из нашей статьи Написание асинхронного кода в Node.js.

      Шаг 1 — Создание базового сервера HTTP

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

      Прежде всего нам нужно настроить доступную среду программирования для выполнения наших упражнений, а также других заданий в настоящей статье. Создайте в терминале папку с именем first-servers:

      Затем откройте эту папку:

      Затем создайте файл для кода:

      Откройте файл в текстовом редакторе. Мы используем редактор nano, потому что он доступен в терминале:

      Для начала мы загрузим модуль http, входящий в стандартную комплектацию установки Node.js. Добавьте следующую строку в hello.js:

      first-servers/hello.js

      const http = require("http");
      

      Модуль http содержит функцию создания сервера, которую мы более детально рассмотрим позднее. Если вы хотите узнать больше о модулях в Node.js, познакомьтесь с нашей статьей Создание модуля Node.js.

      На следующем шаге мы определим две константы, хост и порт, к которым будет привязан наш сервер:

      first-servers/hello.js

      ...
      const host = 'localhost';
      const port = 8000;
      

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

      Значение localhost — это специальный частный адрес, с помощью которого компьютеры ссылаются на себя. Обычно оно эквивалентно внутреннему IP-адресу 127.0.0.1 и доступно только локальному компьютеру, но недоступно Интернету или локальным сетям, к которым подключен компьютер.

      Порт — это числовое значение, которое серверы используют как точку доступа или «дверь» к нашему IP-адресу. В нашем примере мы будем использовать для нашего веб-сервера порт 8000. Порты 8080 и 8000 обычно используются при разработке как порты по умолчанию, и в большинстве случаев разработчики предпочитают использовать именно эти порты для серверов HTTP.

      Когда мы привяжем наш сервер к этому хосту и порту, мы сможем подключаться к нашему серверу, открывая адрес http://localhost:8000 в локальном браузере.

      Добавим специальную функцию, которую в Node.js мы называем прослушиватель запросов. Эта функция предназначена для обработки входящих запросов HTTP и возврата ответа HTTP. Данная функция должна иметь два аргумента, объект запроса и объект ответа. Объект запроса записывает все данные поступающего запроса HTTP. Объект ответа используется для возвращения серверу ответов HTTP.

      Нам нужно, чтобы наш первый сервер возвращал следующее сообщение при попытке доступа к нему: "My first server!".

      Добавим эту функцию:

      first-servers/hello.js

      ...
      
      const requestListener = function (req, res) {
          res.writeHead(200);
          res.end("My first server!");
      };
      

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

      Все функции прослушивания запросов в Node.js принимают два аргумента, req и res (мы можем присвоить им другие имена, если захотим). Отправляемый пользователем запрос HTTP записывается в объекте Request, который соответствует первому аргументу, req. Отправляемый пользователю ответ HTTP формируется посредством взаимодействия с объектом Response во втором аргументе, res.

      Первая строка res.writeHead(200); задает код состояния HTTP для ответа. Коды состояния HTTP показывают, насколько хорошо запрос HTTP обработан сервером. В данном случае код состояния 200 соответствует результату "OK". Если вы хотите узнать больше о различных кодах HTTP, которые могут возвращать ваши веб-серверы, и о значении этих кодов, начните с нашего руководства Диагностика распространенных кодов ошибок HTTP.

      Следующая строка функции, res.end("My first server!") ;, записывает ответ HTTP на клиент, который его запросил. Эта функция возвращает любые данные, которые должен возвращать сервер. В этом случае будут возвращаться текстовые данные.

      Теперь мы можем создать сервер и использовать прослушиватель запросов:

      first-servers/hello.js

      ...
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Сохраните код и закройте nano, нажав CTRL+X.

      В первой строке мы создали новый объект server с помощью функции createServer() модуля http. Этот сервер принимает запросы HTTP и передает их нашей функции requestListener().

      После создания сервера мы должны привязать его к сетевому адресу. Для этого мы используем метод server.listen(). Он принимает три аргумента: port, host и функцию обратного вызова, срабатывающую, когда сервер начинает прослушивание.

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

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

      Примечание. Хотя requestListener() не использует объект req, он должен быть первым аргументом функции.

      Мы создали веб-сервер, написав менее пятнадцати строк кода. Проверим его работу, запустив программу:

      Мы увидим в консоли следующее:

      Output

      Server is running on http://localhost:8000

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

      Мы используем отдельное окно терминала для взаимодействия с сервером с помощью cURL, инструмента командной строки для обмена данными с сетью. Введите команду для отправки запроса HTTP GET на запущенный нами сервер:

      • curl http://localhost:8000

      При нажатии клавиши ENTER на терминале появится следующее:

      Output

      My first server!

      Мы настроили сервер и получили от него первый ответ.

      Теперь давайте подробнее разберемся с тем, что произошло во время тестирования сервера. Мы использовали cURL для отправки запроса GET на сервер с адресом http://localhost:8000. Наш сервер Node.js прослушивал соединения этого адреса. Сервер передал запрос функции requestListener(). Функция вернула текстовые данные с кодом состояния 200. Сервер отправил ответ в cURL, и на нашем терминале появилось сообщение.

      Прежде чем продолжить, нажмем CTRL+C и закроем запущенный сервер. Это прервет работу сервера и вернет нас в командную строку.

      Большинство сайтов и API не используют для ответов формат обычного текста. Страницы HTML и данные JSON —наиболее распространенные форматы ответов. На следующем шаге мы узнаем, как возвращать ответы HTTP в распространенных форматах данных, которые мы встречаем в Интернете.

      Шаг 2 — Возврат разных типов контента

      Возвращаемый веб-сервером ответ может иметь разные форматы. Мы уже упоминали JSON и HTML, но также существуют и другие текстовые форматы, в том числе XML и CSV. Кроме того, веб-серверы могут возвращать данные и не в текстовом формате, в том числе файлы PDF, архивы zip, аудио- и видеофайлы.

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

      Все эти три типа данных основаны на текстовом формате и очень часто используются для распространения контента в Интернете. Многие инструменты и языки разработки для серверов поддерживают возврат этих типов данных. В контексте Node.js нам необходимы две вещи:

      1. Задать для заголовка Content-Type в ответах HTTP подходящее значение.
      2. Убедиться, что res.end() получает данные в правильном формате.

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

      Создайте новый файл с именем html.js. Этот файл будет использоваться позднее для возврата текста HTML в ответе HTTP. Здесь мы введем код шаблона и скопируем его в другие серверы, возвращающие разные типы.

      Введите в терминале следующее:

      Теперь откройте этот файл в текстовом редакторе:

      Скопируем код шаблона. Введите в nano следующее:

      first-servers/html.js

      const http = require("http");
      
      const host = 'localhost';
      const port = 8000;
      
      const requestListener = function (req, res) {};
      
      const server = http.createServer(requestListener);
      server.listen(port, host, () => {
          console.log(`Server is running on http://${host}:${port}`);
      });
      

      Сохраните файл html.js и закройте его с помощью CTRL+X, а затем вернитесь в терминал.

      Теперь скопируем этот файл в два новых файла. Первый файл будет возвращать данные CSV в ответе HTTP:

      Второй файл будет возвращать ответ JSON на сервере:

      Остальные файлы будут предназначены для последующих упражнений:

      • cp html.js htmlFile.js
      • cp html.js routes.js

      Теперь мы готовы продолжить наши упражнения. Начнем с возврата JSON.

      Вывод JSON

      Нотация объектов JavaScript (JSON) представляет собой текстовый формат обмена данными. Как предполагает его название, данный формат основан на объектах JavaScript, но при этом он не зависит от языка, то есть его может использовать любой язык программирования, способный парсить его синтаксис.

      Формат JSON обычно используется API для приема и возврата данных. Его популярность обусловлена меньшим размером, чем у XML и других форматов обмена данными, а также наличием инструментов для парсинга его синтаксиса без излишних усилий. Если вы хотите узнать больше о JSON, вы можете прочитать наше руководство Работа с JSON в JavaScript.

      Откройте файл json.js с помощью nano:

      Нам нужно вернуть ответ JSON. Изменим функцию requestListener() для возврата соответствующего заголовка для всех ответов JSON посредством изменения выделенных строк:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      };
      ...
      

      Метод res.setHeader() добавляет заголовок HTTP к ответу. Заголовки HTTP содержат дополнительную информацию, которая может быть прикреплена к запросу или ответу. Метод res.setHeader() принимает два аргумента: название заголовка и его значение.

      Заголовок Content-Type используется для указания формата данных, который также называется типом носителя и отправляется с запросом или ответом. В этом случае Content-Type имеет значение application/json.

      Возвратим пользователю контент JSON. Изменим json.js следующим образом:

      first-servers/json.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          res.writeHead(200);
          res.end(`{"message": "This is a JSON response"}`);
      };
      ...
      

      Как и ранее, мы сообщаем пользователю об успешном выполнении запроса, возвращая статус 200. Теперь наш аргумент строки в вызове response.end() содержит корректный код JSON.

      Сохраните и закройте json.js, нажав CTRL+X. Запустим сервер с помощью команды node:

      Подключимся к серверу в другом терминале, используя cURL:

      • curl http://localhost:8000

      Нажав ENTER, мы увидим следующий результат:

      Output

      {"message": "This is a JSON response"}

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

      Обслуживание CSV

      Формат разделенных запятой значений (CSV) — это стандартный текстовый формат вывода табличных данных. В большинстве случаев строки разделяются символами новой строки, а элементы внутри строки разделяются запятым.

      Откройте файл csv.js в нашем рабочем пространстве с помощью текстового редактора:

      Добавим следующие строки в функцию requestListener():

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
      };
      ...
      

      Теперь Content-Type имеет значение text/csv, соответствующее формату файлов CSV. Также мы добавим заголовок Content-Disposition. Этот заголовок указывает браузеру способ отображения данных, особенно в браузере или в отдельном файле.

      При возврате ответов CSV большинство современных браузеров автоматически загружают файл, даже если заголовок Content-Disposition не установлен. Однако при возврате файла CSV этот заголовок нужно добавить, поскольку он позволяет нам задать имя файла CSV. Мы сообщаем браузеру, что файл CSV является вложением, и что его следует загрузить. Затем мы сообщаем браузеру, что файлу присвоено имя oceanpals.csv.

      Запишем данные CSV в ответе HTTP:

      first-servers/csv.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/csv");
          res.setHeader("Content-Disposition", "attachment;filename=oceanpals.csv");
          res.writeHead(200);
          res.end(`id,name,emailn1,Sammy Shark,shark@ocean.com`);
      };
      ...
      

      Как и раньше, мы возвращаем статус 200/OK в нашем ответе. Теперь наш вызов res.end() содержит строку корректного файла CSV. Значения в каждом столбце разделяются запятыми, а строки разделяются символом новой строки (n). У нас имеется две строки, одна для заголовка таблицы, а другая — для данных.

      Протестируем этот сервер в браузере. Сохраните файл csv.js и закройте редактор, нажав CTRL+X.

      Запустите сервер с помощью команды Node.js:

      Откроем сервер в другом терминале с помощью cURL:

      • curl http://localhost:8000

      На консоли появится следующее:

      Output

      id,name,email 1,Sammy Shark,shark@ocean.com

      Если мы откроем в браузере адрес http://localhost:8000, загрузится файл CSV. Файл будет иметь имя oceanpals.csv.

      Закройте работающий сервер, нажав CTRL+C для возврата в стандартную командную строку терминала.

      Мы рассмотрели возврат данных в форматах JSON и CSV, которые часто используются в API. Теперь перейдем к возврату данных сайтов, просматриваемых людьми в браузере.

      Обслуживание кода HTML

      Гипертекстовый язык разметки (HTML) — самый распространенный формат, используемый пользователями при взаимодействии с серверами через браузер. Он был создан для структурирования веб-контента. Браузеры разработаны для отображения контента в формате HTML, оформленного с использованием стилей CSS, еще одной клиентской веб-технологии для настройки внешнего вида сайтов.

      Откроем файл html.js в текстовом редакторе еще раз:

      Изменим функцию requestListener() так, чтобы она возвращала подходящий заголовок Content-Type для ответа HTML:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
      };
      ...
      

      Возвратим пользователю контент HTML. Добавьте в файл html.js выделенные строки, чтобы он выглядел следующим образом:

      first-servers/html.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(`<html><body><h1>This is HTML</h1></body></html>`);
      };
      ...
      

      Вначале мы добавляем код состояния HTTP. Затем мы вызываем response.end() с аргументом строки, содержащим корректный код HTML. Открывая сервер в браузере, мы увидим страницу HTML с одним тегом заголовка со значением This is HTML.

      Сохраним файл и закроем редактор, нажав CTRL+X. Запустим сервер с помощью команды node:

      После запуска программы мы увидим сообщение Server is running on http://localhost:8000.

      Откройте в браузере адрес http://localhost:8000. Страница будет выглядеть следующим образом:

      Изображение ответа HTML, возвращаемого сервером Node.js

      Закроем работающий сервер, нажав CTRL+C для возврата в стандартную командную строку терминала.

      Код HTML часто добавляется в файл отдельно от серверного кода, такого как наши программы Node.js. Посмотрим, как можно выводить ответы HTML из файлов.

      Шаг 3 — Вывод страницы HTML из файла

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

      Для вывода файлов HTML мы загружаем их с помощью модуля fs и используем их данные при написании ответа HTTP.

      Вначале создадим файл HTML, который будет возвращать наш веб-сервер. Создайте новый файл HTML:

      Откройте файл index.html в текстовом редакторе:

      Наша веб-страница будет минимальной. Она будет иметь оранжевый фон и содержать текст приветствия в центре. Добавьте в файл следующий код:

      first-servers/index.html

      <!DOCTYPE html>
      
      <head>
          <title>My Website</title>
          <style>
              *,
              html {
                  margin: 0;
                  padding: 0;
                  border: 0;
              }
      
              html {
                  width: 100%;
                  height: 100%;
              }
      
              body {
                  width: 100%;
                  height: 100%;
                  position: relative;
                  background-color: rgb(236, 152, 42);
              }
      
              .center {
                  width: 100%;
                  height: 50%;
                  margin: 0;
                  position: absolute;
                  top: 50%;
                  left: 50%;
                  transform: translate(-50%, -50%);
                  color: white;
                  font-family: "Trebuchet MS", Helvetica, sans-serif;
                  text-align: center;
              }
      
              h1 {
                  font-size: 144px;
              }
      
              p {
                  font-size: 64px;
              }
          </style>
      </head>
      
      <body>
          <div class="center">
              <h1>Hello Again!</h1>
              <p>This is served from a file</p>
          </div>
      </body>
      
      </html>
      

      На этой веб-странице отображается две строки текста: Hello Again! и This is served from a file. Строки отображаются друг над другом в центре страницы. Первая строка текста отображается как заголовок, то есть она будет больше. Вторая строка текста будет немного меньше. Весь текст будет выводиться белым цветом на оранжевом фоне страницы.

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

      Это весь код HTML, который нам нужен, так что теперь можно сохранить и закрыть файл, нажав CTRL+X. Теперь мы можем перейти к коду сервера.

      В этом упражнении мы будем работать с файлом htmlFile.js. Откройте этот файл в текстовом редакторе:

      Поскольку нам нужно прочитать файл, для начала импортируем модуль fs:

      first-servers/htmlFile.js

      const http = require("http");
      const fs = require('fs').promises;
      ...
      

      Этот модуль содержит функцию readFile(), которую мы будем использовать для загрузки файла HTML. Мы импортируем вариант обещания в соответствии с современными передовыми практиками работы с JavaScript. Мы используем обещания, поскольку с синтаксической точки зрения они лучше функций обратного вызова, к которым нам пришлось бы прибегнуть, если бы мы назначили fs как require('fs'). Дополнительную информацию о лучших практиках асинхронного программирования можно найти в нашем руководстве Написание асинхронного кода в Node.js.

      Нам нужно, чтобы при отправке пользователем запроса к системе считывался наш файл HTML. Для начала изменим requestListener() для чтения файла:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
      };
      ...
      

      Мы используем метод fs.readFile() для загрузки файла. Он использует аргумент __dirname + "/index.html". Специальная переменная __dirname содержит абсолютный путь к директории запуска кода Node.js. В конце мы добавляем /index.html, чтобы мы могли загрузить ранее созданный файл HTML.

      Возвратим страницу HTML после ее загрузки:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
      };
      ...
      

      Если обещание fs.readFile() успешно выполняется, оно возвращает свои данные. Для этого случая мы используем метод then(). Параметр contents содержит данные файла HTML.

      Вначале мы задаем для заголовка Content-Type значение text/html, чтобы сообщить клиенту, что мы возвращаем данные HTML. Затем мы пишем код состояния, показывая, что запрос выполнен успешно. В заключение мы отправляем на клиент загруженную страницу HTML с данными в переменной contents.

      Иногда метод fs.readFile() может выполняться с ошибками, и нам нужно предусмотреть подобные случаи. Добавьте в функцию requestListener() следующее:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          fs.readFile(__dirname + "/index.html")
              .then(contents => {
                  res.setHeader("Content-Type", "text/html");
                  res.writeHead(200);
                  res.end(contents);
              })
              .catch(err => {
                  res.writeHead(500);
                  res.end(err);
                  return;
              });
      };
      ...
      

      Сохраните файл и закройте nano, нажав CTRL+X.

      Когда в обещании возникает ошибка, оно отклоняется. Эта ситуация обрабатывается с помощью метода catch(). Он принимает ошибку, возвращаемую fs.readFile(), устанавливает код состояния 500, сигнализирующий о внутренней ошибке, и возвращает пользователю сообщение об ошибке.

      Запустите наш сервер с помощью команды node:

      Откройте в браузере адрес http://localhost:8000. Вы увидите следующую страницу:

      Изображение страницы HTML, загруженной из файла в Node.js

      Мы вывели пользователю страницу HTML с сервера. Теперь мы можем закрыть запущенный сервер, нажав CTRL+C. Сделав это, мы увидим командную строку терминала.

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

      Чтобы продемонстрировать этот метод, покажем, как можно сделать сервер более эффективным и масштабируемым.

      Эффективный вывод кода HTML

      Вместо того чтобы загружать страницу HTML для каждого запроса, мы загрузим ее только один раз, в самом начале. Запрос будет возвращать данные, загруженные нами при запуске.

      Откройте в терминале скрипт Node.js с помощью текстового редактора:

      Добавим новую переменную, прежде чем создавать функцию requestListener():

      first-servers/htmlFile.js

      ...
      let indexFile;
      
      const requestListener = function (req, res) {
      ...
      

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

      Изменим функцию requestListener(). Теперь вместо загрузки файла она будет возвращать содержимое indexFile:

      first-servers/htmlFile.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "text/html");
          res.writeHead(200);
          res.end(indexFile);
      };
      ...
      

      Далее мы изменим логику чтения файла с функции requestListener() на момент запуска нашего сервера. Внесите следующие изменения при создании сервера:

      first-servers/htmlFile.js

      ...
      
      const server = http.createServer(requestListener);
      
      fs.readFile(__dirname + "/index.html")
          .then(contents => {
              indexFile = contents;
              server.listen(port, host, () => {
                  console.log(`Server is running on http://${host}:${port}`);
              });
          })
          .catch(err => {
              console.error(`Could not read index.html file: ${err}`);
              process.exit(1);
          });
      

      Сохраните файл и закройте nano, нажав CTRL+X.

      Код, считывающий файл, похож на написанный нами при первой попытке. Однако при успешном чтении файла мы можем сохранить его содержимое в глобальной переменной indexFile. Мы запустим сервер с методом listen(). Главное — загрузить файл до запуска сервера. Так функция requestListener() гарантированно возвращает страницу HTML, поскольку переменная indexFile больше не пустая.

      Блок обработки ошибок также изменился. Если файл не удается загрузить, мы записываем ошибку и выводим ее на консоль. Затем мы закрываем программу Node.js с помощью функции exit() без запуска сервера. Так мы видим, почему не удалось прочитать файл, и можем решить проблему и снова запустить сервер.

      Мы создали разные веб-серверы, возвращающие пользователю разные типы данных. Пока что мы не использовали данные запросов для определения конкретного возвращаемого контента. Нам потребуется использовать данные запросов при настройке маршрутов или путей сервера Node.js, так что теперь мы посмотрим, как это работает.

      Шаг 4 — Управление маршрутами с использованием объекта HTTP Request

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

      Хотя данные книг и авторов связаны, они представляют собой разные объекты. В подобных случаях разработчики обычно программируют каждый объект для разных конечных точек, чтобы показать пользователю API, с какими данными он взаимодействует.

      Создадим новый сервер для небольшой библиотеки, который будет возвращать два разных типа данных. Если пользователь откроет адрес сервера с /books, он получит список книг в формате JSON. Если пользователь откроет раздел /authors, он получит список с информацией об авторах в формате JSON.

      До сих пор мы возвращали одинаковые ответы на каждый получаемый запрос. Рассмотрим небольшой пример.

      Запустим заново наш пример с ответом JSON:

      Отправим на другом терминале запрос cURL, как мы делали ранее:

      • curl http://localhost:8000

      Вы увидите следующее:

      Output

      {"message": "This is a JSON response"}

      Теперь попробуем другую команду curl:

      • curl http://localhost:8000/todos

      После нажатия Enter вы увидите тот же самый результат:

      Output

      {"message": "This is a JSON response"}

      Мы не встраивали в функцию requestListener() никакую специальную логику дял обработки запроса, URL которого содержит /todos, и поэтому Node.js по умолчанию возвращает то же сообщение JSON.

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

      Для начала закройте сервер, нажав CTRL+C.

      Откройте файл routes.js в своем текстовом редакторе:

      Начнем с сохранения наших данных JSON в переменных перед функцией requestListener():

      first-servers/routes.js

      ...
      const books = JSON.stringify([
          { title: "The Alchemist", author: "Paulo Coelho", year: 1988 },
          { title: "The Prophet", author: "Kahlil Gibran", year: 1923 }
      ]);
      
      const authors = JSON.stringify([
          { name: "Paulo Coelho", countryOfBirth: "Brazil", yearOfBirth: 1947 },
          { name: "Kahlil Gibran", countryOfBirth: "Lebanon", yearOfBirth: 1883 }
      ]);
      ...
      

      Переменная books — это строка, содержащая данные JSON для массива объектов книг. Каждая книга имеет заголовок или название, автора и год издания.

      Переменная authors — это строка, содержащая данные JSON для массива объектов авторов. Каждый автор имеет имя, страну рождения и год рождения.

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

      Вначале нужно убедиться, что все ответы нашего сервера будут иметь правильный заголовок Content-Type:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
      }
      ...
      

      Далее нам нужно возвращать подходящие данные JSON в зависимости от URL, используемого пользователем. Создадим выражение switch для URL запроса:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {}
      }
      ...
      

      Чтобы получить путь URL от объекта request, нам потребуется доступ к его свойству url. Теперь мы можем добавить в выражение switch варианты для возврата подходящего кода JSON.

      Выражение switch в JavaScript позволяет определять, какой код будет выполняться в зависимости от значения объекта или выражения JavaScript (например, от результата математической операции). Если вы не знаете или забыли, как использовать такие выражения, воспользуйтесь нашим руководством Использование выражения switch в JavaScript.

      Продолжим и добавим вариант, когда пользователь хочет получить список книг:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
          }
      }
      ...
      

      Мы устанавливаем код состояния 200, указывая, что запрос обработан правильно, и возвращаем данные JSON, содержащие список книг. Добавим еще один вариант для авторов:

      first-servers/routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
          }
      }
      ...
      

      Как и раньше, мы используем код состояния 200 для подтверждения правильного выполнения запроса. Теперь мы возвращаем данные JSON со списком авторов.

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

      routes.js

      ...
      const requestListener = function (req, res) {
          res.setHeader("Content-Type", "application/json");
          switch (req.url) {
              case "/books":
                  res.writeHead(200);
                  res.end(books);
                  break
              case "/authors":
                  res.writeHead(200);
                  res.end(authors);
                  break
              default:
                  res.writeHead(404);
                  res.end(JSON.stringify({error:"Resource not found"}));
          }
      }
      ...
      

      Мы используем ключевое слово default в выражении switch, чтобы учесть все остальные сценарии, кроме описанных в предыдущих вариантах. Мы устанавливаем код состояния 404, указывающий, что запрошенный URL не найден. Далее мы задаем объект JSON, содержащий сообщение об ошибке.

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

      • curl http://localhost:8000/books

      Нажмите Enter, чтобы получить следующий результат:

      Output

      [{"title":"The Alchemist","author":"Paulo Coelho","year":1988},{"title":"The Prophet","author":"Kahlil Gibran","year":1923}]

      Пока все хорошо. Попробуем то же самое для /authors. Введите в терминале следующую команду:

      • curl http://localhost:8000/authors

      После выполнения команды вы увидите следующее:

      Output

      [{"name":"Paulo Coelho","countryOfBirth":"Brazil","yearOfBirth":1947},{"name":"Kahlil Gibran","countryOfBirth":"Lebanon","yearOfBirth":1883}]

      В заключение попробуем ввести ошибочный URL, чтобы функция requestListener() вывела сообщение об ошибке:

      • curl http://localhost:8000/notreal

      При вводе этой команды будет выведено следующее сообщение:

      Output

      {"error":"Resource not found"}

      Закройте работающий сервер, нажав CTRL+C.

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

      Заключение

      В этом обучающем руководстве мы создали несколько серверов HTTP Node.js. Вначале мы сформировали простой текстовый ответ. Затем мы перешли к возврату с сервера разных типов данных: JSON, CSV и HTML. Затем мы рассмотрели комбинирование загрузки файлов с ответами HTTP для вывода пользователю страниц HTML с сервера и создания API, использующего данные запроса пользователя для определения ответа.

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

      Чтобы узнать больше о веб-серверах HTTP в Node.js, вы можете почитать документацию Node.js по модулю http. Если вы хотите продолжить изучение Node.js, возвращайтесь на страницу серии Программирование в Node.js.



      Source link

      Тестирование модуля Node.js с использованием Mocha и Assert


      Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

      Введение

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

      Мы создаем примеры тестирования с помощью структур тестовых фреймворков. Mocha — это популярный тестовый фреймворк JavaScript, используемый для организации и запуска тестовых файлов. Однако Mocha не подтверждает поведение нашего кода. Для сравнения значений в тесте мы можем использовать модуль Node.js assert​​​​​​.

      В этой статье вы узнаете, как написать тесты для списка дел (TODO) для модуля Node.js. Для создания тестов будет настроен и использован фреймворк Mocha. Также будет использован модуль Node.js assert для создания самих тестов. В этом смысле вы будете использовать Mocha в качестве планировщика, а assert​​​ для реализации плана.

      Предварительные требования

      • Node.js, установленный на вашем компьютере для разработки. В этом обучающем руководстве используется версия Node.js 10.16.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04.
      • Базовые знания JavaScript, которые можно получить из нашей серии статей Программирование на JavaScript.

      Шаг 1 — Создание модуля Node

      Давайте начнем с написания модуля Node.js, который мы будем тестировать. Этот модуль будет управлять списком элементов TODO. Используя этот модуль, мы сможем перечислить все элементы списка TODO, которые нужно отследить, добавить новые элементы и отметить некоторые как выполненные. Также мы сможем экспортировать список элементов TODO в файл CSV. Если вам нужно вспомнить, как писать модули Node.js, прочтите нашу статью Создание модуля Node.js.

      Для начала необходимо настроить среду программирования. Создайте папку с именем проекта в своем терминале. В данном обучающем руководстве будет использоваться имя todos:

      Затем откройте эту папку:

      Теперь инициализируйте npm, поскольку позже мы будем использовать его функцию командной строки для запуска тестирования:

      У нас есть только одна зависимость, Mocha, которую мы будем использовать для организации и запуска тестов. Для загрузки и установки Mocha воспользуйтесь следующей командой:

      • npm i request --save-dev mocha

      Мы установим Mocha как зависимость dev, поскольку это не требуется модулем в производственных настройках. Если вы хотите узнать больше о пакетах Node.js или npm, ознакомьтесь с руководством Использование модулей Node.js с npm и package.json.

      Наконец, создадим файл, который будет содержать код нашего модуля:

      Теперь мы готовы создать наш модуль. Откройте index.js​​​ в текстовом редакторе, например nano:

      Давайте начнем с определения класса Todos. Этот класс содержит все функции, необходимые для управления нашим списком TODO. Добавьте следующие строки кода в index.js:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      }
      
      module.exports = Todos;
      

      Начнем с создания класса Todos. Его функция constructor() не принимает аргументов, поэтому нам не нужно предоставлять значения для создания объекта для данного класса. Все, что мы делаем, когда инициализируем объект Todos, — это создаем свойство todos, которое является пустым массивом.

      Линия модулей позволяет другим модулям Node.js требовать наш класс Todos. Без прямого экспорта класса тестовый файл, который мы создадим позже, не сможет использовать его.

      Давайте добавим функцию для возврата сохраненного массива todos. Запишите следующие выделенные строки:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      }
      
      module.exports = Todos;
      

      Функция list() возвращает копию массива, используемого классом. Она делает копию массива, используя деструктурирующий синтаксис JavaScript. Мы создаем копию массива, чтобы изменения, которые пользователь вносит в массив, возвращенный функцией list(), не влияли на массив, используемый объектом Todos.

      Примечание. Массивы JavaScript — это справочные файлы. Это значит, что для любого присваивания переменной для массива или вызова функции с массивом в качестве параметра JavaScript обращается к оригинальному созданному массиву. Например, если у нас есть массив с тремя элементами с именем x и мы создаем новую переменную y, так что y = x, y и x относятся к одному и тому же. Все изменения, выполняемые для массива с y, влияют на переменную x и наоборот.

      Теперь создадим функцию add(), которая добавляет новый элемент TODO:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      }
      
      module.exports = Todos;
      

      Наша функция add() берет строку и помещает ее в свойство title нового объекта JavaScript. Новый объект также имеет свойство completed, которое по умолчанию устанавливается на false. Затем мы добавляем этот новый объект к нашему массиву TODO.

      Важной функцией в менеджере TODO является отметка элементов как завершенные. Для выполнения этой задачи мы пройдем в цикле по нашему массиву todos, чтобы найти элемент TODO, который ищет пользователь. Если элемент найден, отметим его как завершенный. Если ничего не найдено, выдадим ошибку.

      Добавьте функцию complete()​​​ следующим образом:

      todos/index.js

      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
      
              this.todos.push(todo);
          }
      
          complete(title) {
              let todoFound = false;
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      }
      
      module.exports = Todos;
      

      Сохраните файл и выйдите из текстового редактора.

      Теперь у нас есть базовый менеджер TODO, с которым можно экспериментировать. Далее проверим код вручную, чтобы убедиться в работе приложения.

      Шаг 2 — Ручное тестирование кода

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

      Добавим в наше приложение два элемента TODO и отметим один из них как завершенный. Запустите Node.js REPL в той же папке, что и файл index.js:

      Вы увидите командную строку > в REPL, которая указывает, что мы можем ввести код JavaScript. Введите в командную строку следующее:

      • const Todos = require('./index');

      С помощью require() мы загружаем модуль TODO в переменную Todos. Помните, что наш модуль возвращает класс Todos по умолчанию.

      Теперь инстанцируем объект для этого класса. В REPL добавьте следующую строку кода:

      • const todos = new Todos();

      Мы можем использовать объект todos для проверки работы реализации. Добавим первый элемент TODO:

      До сих пор мы не видели никаких выводов в нашем терминале. Давайте убедимся, что мы сохранили элемент TODO run code, получив список всех наших TODO:

      Вы увидите следующий вывод в вашем REPL:

      Output

      [ { title: 'run code', completed: false } ]

      Это ожидаемый результат: у нас есть один элемент TODO в нашем массиве TODO, и он не завершен по умолчанию.

      Добавим другой элемент TODO:

      • todos.add("test everything");

      Отметим первый элемент TODO как завершенный:

      • todos.complete("run code");

      Теперь наш объект todos будет управлять двумя элементами: run code и test everything. TODO run code также будет завершен. Подтвердим это, вызвав list()​​​ еще раз:

      Вывод REPL будет выглядеть следующим образом:

      Output

      [ { title: 'run code', completed: true }, { title: 'test everything', completed: false } ]

      Теперь закройте REPL следующим образом:

      Мы подтвердили, что наш модуль работает соответствующим образом. Хотя мы не поместили наш код в тестовый файл и не использовали тестовую библиотеку, мы вручную протестировали код. К сожалению, эта форма тестирования займет много времени, если ее использовать при выполнении каждого изменения. Далее попробуем выполнить автоматизированное тестирование в Node.js и посмотрим, возможно ли решить данную проблему с помощью тестового фреймворка Mocha.

      Шаг 3 — Создание первого теста с помощью Mocha и Assert

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

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

      В этом обучающем руководстве мы используем тестовый фреймворк Mocha с модулем Node.js assert​​​. Давайте на практике посмотрим, как они вместе работают.

      Для начала создадим новый файл для хранения кода теста:

      Теперь с помощью предпочтительного текстового редактора откройте файл тестирования. Можно использовать nano, как раньше:

      В первой строке текстового файла мы загрузим модуль TODO аналогично тому, как мы делали в оболочке Node.js. Затем мы загрузим модуль assert​​​, чтобы он был на момент создания тестов. Добавьте следующие строки:

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      

      Свойство strict​​​​ модуля assert позволит нам использовать специальные тесты эквивалентности, рекомендуемые Node.js, которые также подходят для проверок в дальнейшем, поскольку отвечают за большее число вариантов использования.

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

      describe([String with Test Group Name], function() {
          it([String with Test Name], function() {
              [Test Code]
          });
      });
      

      Обратите внимание на две ключевые функции: describe() и it()​​​. Функция describe() используется для группировки аналогичных тестов. Для Mocha не требуется запускать тесты, но их группировка упростит поддержку нашего кода теста. Рекомендуется группировать тесты таким образом, чтобы было проще обновлять аналогичные вместе.

      it() содержит наш код теста. Именно здесь мы могли бы взаимодействовать с функциями нашего модуля и использовать библиотеку assert​​. Многие функции it() могут быть определены в функции describe().

      Цель этого раздела состоит в использовании Mocha и assert для автоматизации нашего ручного теста. Мы будем делать это постепенно, начав с блока описания. Добавьте в файл следующее после строк модуля:

      todos/index.test.js

      ...
      describe("integration test", function() {
      });
      

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

      Давайте добавим функцию it(), чтобы начать тестирование нашего кода модуля:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
          });
      });
      

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

      Для нашего первого теста мы создадим новый объект Todos и проверим, что в нем нет элементов:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Первая новая строка кода инстанциировала новый объект Todos, как мы делали в Node.js REPL или другом модуле. Во второй новой строке мы использовали модуль assert​​​.

      Из модуля assert мы используем метод notStrictEqual()​​​. Эта функция учитывает два параметра: значение, которое необходимо протестировать (называется фактическое значение), и значение, которое мы ожидаем получить (называется ожидаемое значение). Если эти оба аргумента одинаковы, notStrictEqual()​​​ выдает ошибку о непрохождении теста.

      Сохраните и закройте index.test.js.

      Базовый сценарий будет истинным, так как длина должна быть 0, что не равно 1. Давайте убедимся в этом, запустив Mocha. Для этого нам потребуется модифицировать наш файл package.json. Откройте файл package.json в своем текстовом редакторе:

      Теперь в свойстве scripts измените его следующим образом:

      todos/package.json

      ...
      "scripts": {
          "test": "mocha index.test.js"
      },
      ...
      

      Мы только что изменили поведение команды test командной строки npm. Когда мы запустим npm test, npm проверит команду, которую мы только что ввели в package.json. Он будет искать библиотеку Mocha в нашей папке node_modules​​​ и запустит команду mocha с нашим файлом тестирования.

      Сохраните и закройте package.json.

      Давайте посмотрим, что происходит, когда мы запускаем наш тест. В своем терминале введите:

      Команда выдаст следующий вывод:

      Output

      > todos@1.0.0 test your_file_path/todos > mocha index.test.js integrated test ✓ should be able to add and complete TODOs 1 passing (16ms)

      Этот вывод сначала покажет нам, какая группа тестов сейчас запустится. Для каждого отдельного теста в группе тестовый сценарий является ступенчатым. Мы видим наше имя теста так, как мы описали его в функции it(). Галочка с левой стороны тестового сценария указывает на то, что тест пройден.

      Внизу мы получим резюме всех наших тестов. В нашем случае один тест был выполнен и завершен в течение 16 мс (время зависит от компьютера).

      Тестирование началось успешно. Однако текущий тестовый сценарий может допускать ложные позитивные результаты. Ложные позитивные результаты — это тестовый сценарий, когда тест пройден тогда, когда не должен.

      Теперь мы проверяем, что длина массива не равна 1. Давайте изменим тест, чтобы это условие было истинным, когда не должно. Добавьте следующие строки в index.test.js​​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.notStrictEqual(todos.list().length, 1);
          });
      });
      

      Сохраните и закройте файл.

      Мы добавили два элемента TODO. Давайте запустим тест, чтобы увидеть, что произойдет:

      В результате вы получите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (8ms)

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

      Давайте изменим тест таким образом, что его успешное прохождение будет возможным только при полном отсутствии TODO в памяти. Выполните следующие изменения в index.test.js​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              todos.add("get up from bed");
              todos.add("make up bed");
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Вы изменили notStrictEqual()​​​​​​ на strictEqual()​​​, функцию, которая проверяет эквивалентность между фактическим и ожидаемым аргументом. Строгое равенство (Strict equal) завершится неудачей, если наши аргументы не полностью одинаковы.

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

      В этот раз вывод покажет ошибку:

      Output

      ... integration test 1) should be able to add and complete TODOs 0 passing (16ms) 1 failing 1) integration test should be able to add and complete TODOs: AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B: + expected - actual - 2 + 0 + expected - actual -2 +0 at Context.<anonymous> (index.test.js:9:10) npm ERR! Test failed. See above for more details.

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

      Резюме теста находится уже не внизу вывода, а сразу после нашего списка отображенных тестовых сценариев:

      ...
      0 passing (29ms)
        1 failing
      ...
      

      В остальной части вывода предоставлены данные о непройденных тестах. Сначала мы видим, какие тестовые сценарии не пройдены:

      ...
      1) integrated test
             should be able to add and complete TODOs:
      ...
      

      Затем мы увидим причину непрохождения теста:

      ...
            AssertionError [ERR_ASSERTION]: Input A expected to strictly equal input B:
      + expected - actual
      
      - 2
      + 0
            + expected - actual
      
            -2
            +0
      
            at Context.<anonymous> (index.test.js:9:10)
      ...
      

      Выдается AssertionError, когда не выполняется strictEqual()​​​. Мы видим, что ожидаемое значение 0 отличается от фактического значения 2.

      Затем мы увидим строку в нашем файле тестирования, где код не выполняется. В этом случае это строка 10.

      Теперь мы воочию убедились, что наш тест не будет пройден, если мы будем ожидать некорректные результаты. Давайте изменим наш тестовый сценарий обратно на правильное значение. Откройте файл:

      Затем выберите строки todos.add, чтобы ваш код выглядел следующим образом:

      todos/index.test.js

      ...
      describe("integration test", function () {
          it("should be able to add and complete TODOs", function () {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
          });
      });
      

      Сохраните и закройте файл.

      Запустите его еще раз, чтобы убедиться в прохождении без каких-либо ложных позитивных результатов:

      Вывод будет выглядеть следующим образом:

      Output

      ... integration test ✓ should be able to add and complete TODOs 1 passing (15ms)

      Теперь мы значительно улучшили отказоустойчивость нашего теста. Давайте перейдем к нашему интеграционному тесту. Следующий шаг — добавить новый элемент TODO в index.test.js​​​:

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
          });
      });
      

      После использования функции add() мы подтверждаем, что у нас есть один элемент TODO, управляемый нашим объектом todos, при этом мы будем использовать strictEqual()​​. Наш следующий тест подтверждает данные в todos с помощью deepStrictEqual(). Функция deepStrictEqual() рекурсивно проверяет, имеют ли наши предполагаемые и реальные объекты одни и те же свойства. В этом случае проверяется, содержат ли оба ожидаемых массива объект JavaScript. Затем проверяется, имеют ли эти объекты JavaScript одинаковые свойства, т. е. оба их свойства title — это run code, а оба свойства completedfalse.

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

      todos/index.test.js

      ...
      describe("integration test", function() {
          it("should be able to add and complete TODOs", function() {
              let todos = new Todos();
              assert.strictEqual(todos.list().length, 0);
      
              todos.add("run code");
              assert.strictEqual(todos.list().length, 1);
              assert.deepStrictEqual(todos.list(), [{title: "run code", completed: false}]);
      
              todos.add("test everything");
              assert.strictEqual(todos.list().length, 2);
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: false },
                      { title: "test everything", completed: false }
                  ]
              );
      
              todos.complete("run code");
              assert.deepStrictEqual(todos.list(),
                  [
                      { title: "run code", completed: true },
                      { title: "test everything", completed: false }
                  ]
          );
        });
      });
      

      Сохраните и закройте файл.

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

      Давайте еще раз запустим тест npm test для получения данного вывода:

      Output

      ... integrated test ✓ should be able to add and complete TODOs 1 passing (9ms)

      Вы настроили комплексный тест с помощью Mocha и библиотеки assert.

      Рассмотрим ситуацию, когда мы разделили наш модуль с другими разработчиками, и теперь они предоставляют нам обратную связь. Большинство пользователей хотели бы, чтобы функция complete() возвращала ошибку, в случае если ни один элемент TODO еще не добавлен. Добавим это свойство в функцию complete().

      Откройте index.js в редакторе:

      Добавьте в функцию следующее:

      todos/index.js

      ...
      complete(title) {
          if (this.todos.length === 0) {
              throw new Error("You have no TODOs stored. Why don't you add one first?");
          }
      
          let todoFound = false
          this.todos.forEach((todo) => {
              if (todo.title === title) {
                  todo.completed = true;
                  todoFound = true;
                  return;
              }
          });
      
          if (!todoFound) {
              throw new Error(`No TODO was found with the title: "${title}"`);
          }
      }
      ...
      

      Сохраните и закройте файл.

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

      Вернитесь в index.test.js​​:

      В конце файла добавьте следующий код:

      todos/index.test.js

      ...
      describe("complete()", function() {
          it("should fail if there are no TODOs", function() {
              let todos = new Todos();
              const expectedError = new Error("You have no TODOs stored. Why don't you add one first?");
      
              assert.throws(() => {
                  todos.complete("doesn't exist");
              }, expectedError);
          });
      });
      

      Снова используем describe() и it()​​. Этот тест начинается с создания нового объекта todos. Затем мы определяем ошибку, которую ожидаем получить при вызове функции complete().

      Далее используем функцию throws() модуля assert. Эта функция была создана для проверки ошибок, которые выдаются в коде. Первый аргумент — это функция, содержащая код, который выдает ошибку. Второй аргумент — это ошибка, которую мы ожидаем получить.

      Снова запустите тест с помощью npm test​​​ в своем терминале и вы увидите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs 2 passing (25ms)

      Этот вывод подтверждает преимущества автоматизированного тестирования с помощью Mocha и assert. Поскольку наши тесты выполняются скриптами, каждый раз, когда мы запускаем npm test, мы проверяем, что все тесты успешно пройдены. Нам не нужно вручную проверять, работает ли другой код. Мы знаем, что работает, так как наш тест успешно пройден.

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

      Шаг 4 — Тестирование асинхронного кода

      Одна из функций, описанных в нашем модуле TODO, — это функция экспорта CSV. Она выводит все элементы TODO, а также завершенный статус в файл. Для этого требуется использовать модуль fs — встроенный модуль Node.js для работы с файловой системой.

      Запись в файл — это асинхронная операция. В Node.js есть много способов записи в файл. Можно использовать обратные вызовы, обещания или ключевые слова async/await. В этом разделе мы рассмотрим, как записывать тесты для разных методов.

      Обратные вызовы

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

      Добавим функцию в наш класс Todos с именем saveToFile(). Эта функция будет создавать строку, проходя циклом через все элементы TODO и записывая эту строку в файл.

      Откройте файл index.js:

      Добавьте в этот файл следующий выделенный код:

      todos/index.js

      const fs = require('fs');
      
      class Todos {
          constructor() {
              this.todos = [];
          }
      
          list() {
              return [...this.todos];
          }
      
          add(title) {
              let todo = {
                  title: title,
                  completed: false,
              }
              this.todos.push(todo);
          }
      
          complete(title) {
              if (this.todos.length === 0) {
                  throw new Error("You have no TODOs stored. Why don't you add one first?");
              }
      
              let todoFound = false
              this.todos.forEach((todo) => {
                  if (todo.title === title) {
                      todo.completed = true;
                      todoFound = true;
                      return;
                  }
              });
      
              if (!todoFound) {
                  throw new Error(`No TODO was found with the title: "${title}"`);
              }
          }
      
          saveToFile(callback) {
              let fileContents = 'Title,Completedn';
              this.todos.forEach((todo) => {
                  fileContents += `${todo.title},${todo.completed}n`
              });
      
              fs.writeFile('todos.csv', fileContents, callback);
          }
      }
      
      module.exports = Todos;
      

      Сначала необходимо импортировать модуль fs в наш файл. Затем добавляем новую функцию saveToFile()​​. Эта функция выполняет функцию обратного вызова, которая активируется сразу после завершения операции записи файла. В этой функции мы создаем переменную fileContents, содержащую всю строку, которую мы хотим сохранить в качестве файла. Она активируется с помощью заголовков CSV. Затем проходим циклом через каждый элемент TODO с помощью метода внутреннего массива forEach(). В процессе итерации добавляем свойства title и completed отдельных объектов todos.

      Наконец, используем модуль fs для записи файла с помощью функции writeFile(). Первый аргумент — это имя файла: todos.csv. Второй — это содержимое файла, в этом случае fileContents — это переменная. Последний аргумент — это наша функция обратного вызова, которая обрабатывает любые ошибки записи файла.

      Сохраните и закройте файл.

      Теперь напишем тест для функции saveToFile(). Этот тест выполняет две функции: в первую очередь подтверждает наличие файла, а затем проверяет, имеет ли файл правильное содержимое.

      Откройте файл index.test.js:

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

      todos/index.test.js

      const Todos = require('./index');
      const assert = require('assert').strict;
      const fs = require('fs');
      ...
      

      Теперь в конце файла добавим новый тест:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function(done) {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.saveToFile((err) => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
                  done(err);
              });
          });
      });
      

      Как и ранее, используем команду describe() для группировки нашего теста отдельно от других, так как он подразумевает новую функцию. Функция it() несколько отличается от других функций. Обычно у используемой нами функции обратного вызова нет аргументов. В этот раз у нас есть done в качестве аргумента. Этот аргумент требуется при тестировании функций с обратными вызовами. Функция обратного вызова done() используется Mocha для информирования о завершении асинхронной функции.

      Все функции обратного вызова, протестированные в Mocha, должны вызывать обратный вызов done(). Если нет, Mocha не будет знать, когда функция была завершена, и зависнет в ожидании сигнала.

      Далее создаем экземпляр Todos и добавляем в него один элемент. Затем вызываем функцию saveToFile()​​​ с обратным вызовом, который фиксирует ошибку записи файла. Обратите внимание, как тест для этой функции располагается в обратном вызове. Если бы код теста был за пределами обратного вызова, тест бы не прошел, так как код вызывался до завершения записи файла.

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

      todos/index.test.js

      ...
      assert.strictEqual(fs.existsSync('todos.csv'), true);
      ...
      

      Функция fs.existsSync() возвращает true, если путь файла в аргументе существует, и false, если нет.

      Примечание. Функции модуля fs — асинхронные по умолчанию. Однако для ключевых функций существуют синхронные копии. Этот тест упрощен с помощью синхронных функций, так как нам не нужно встраивать асинхронный код для проверки работы теста. В модуле fs синхронные функции, как правило, имеют Sync ​​​в конце имен.

      Затем создаем переменную для хранения ожидаемого значения:

      todos/index.test.js

      ...
      let expectedFileContents = "Title,Completednsave a CSV,falsen";
      ...
      

      Используем readFileSync() модуля fs для синхронного чтения файла:

      todos/index.test.js

      ...
      let content = fs.readFileSync("todos.csv").toString();
      ...
      

      Теперь предоставляем readFileSync() правильный путь для файла: todos.csv​​. Поскольку readFileSync() возвращает буферный объект Buffer, который хранит бинарные данные, мы используем метод toString() для сравнения его значения со строкой, которую мы предположительно сохранили.

      Как и ранее, используем strictEqual модуля assert для выполнения сравнения:

      todos/index.test.js

      ...
      assert.strictEqual(content, expectedFileContents);
      ...
      

      Заканчиваем тест вызовом обратного вызова done()​​​, чтобы убедиться, что Mocha знает, что нужно остановить тестирование:

      todos/index.test.js

      ...
      done(err);
      ...
      

      Мы указываем объект err в done(), тогда Mocha не пройдет тест в случае возникновения ошибки.

      Сохраните и закройте index.test.js.

      Запускаем этот тест с помощью npm test, как и ранее. Вы увидите следующий вывод на консоли:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (15ms)

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

      Обещания

      Обещание — это объект JavaScript, который в конечном счете возвращает значение. Когда обещание успешно, оно разрешено. Когда встречается ошибка, оно отклоняется.

      Давайте изменим функцию saveToFile()​​ таким образом, чтобы она использовала обещания вместо обратных вызовов. Откройте index.js​​:

      Сначала нам нужно изменить загрузку модуля fs. В вашем файле index.js измените выражение require() в верхней части файла, чтобы это выглядело следующим образом:

      todos/index.js

      ...
      const fs = require('fs').promises;
      ...
      

      Мы только что импортировали модуль fs, который использует обещания, а не обратные вызовы. Теперь нам нужно внести некоторые изменения в команду saveToFile(), чтобы она работала с обещаниями.

      В вашем текстовом редакторе внесите в функцию saveToFile() следующие изменения для удаления обратных вызовов:

      todos/index.js

      ...
      saveToFile() {
          let fileContents = 'Title,Completedn';
          this.todos.forEach((todo) => {
              fileContents += `${todo.title},${todo.completed}n`
          });
      
          return fs.writeFile('todos.csv', fileContents);
      }
      ...
      

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

      Сохраните и закройте index.js.

      Теперь давайте изменим наш тест так, чтобы он работал с обещаниями. Откройте index.test.js​​:

      Замените тест saveToFile()​​​ на следующее:

      todos/index.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", function() {
              let todos = new Todos();
              todos.add("save a CSV");
              return todos.saveToFile().then(() => {
                  assert.strictEqual(fs.existsSync('todos.csv'), true);
                  let expectedFileContents = "Title,Completednsave a CSV,falsen";
                  let content = fs.readFileSync("todos.csv").toString();
                  assert.strictEqual(content, expectedFileContents);
              });
          });
      });
      

      Первое, что нужно изменить, — это удалить обратный вызов done() из аргументов. Если Mocha передает аргумент done(), его необходимо вызвать или он выдаст ошибку такого типа:

      1) saveToFile()
             should save a single TODO:
           Error: Timeout of 2000ms exceeded. For async tests and hooks, ensure "done()" is called; if returning a Promise, ensure it resolves. (/home/ubuntu/todos/index.test.js)
            at listOnTimeout (internal/timers.js:536:17)
            at processTimers (internal/timers.js:480:7)
      

      При тестировании обещаний не включайте обратный вызов done() в it().

      Для проверки обещания нам нужно задать код утверждения в функцию then(). Обратите внимание, что мы возвращаем это обещание в тест, и у нас нет функции catch() для перехвата при отклонении обещания.

      Мы возвращаем обещание, чтобы любые ошибки, выданные в функции then(), всплыли в функции it(). Если ошибки не всплывают, Mocha не провалит тест. При тестировании обещаний вам нужно использовать return для тестируемого обещания. Если нет, вы рискуете получить ложный позитивный результат.

      Также мы пропускаем выражение catch(), так как Mocha может обнаружить, когда обещание отклоняется. При отклонении тест автоматически проваливается.

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

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (18ms)

      Мы изменили наш код и тест для использования обещаний, и теперь мы точно знаем, что это работает. Но последние асинхронные модели используют ключевые слова async/await, поэтому нам не нужно создавать множественные функции then() для обработки успешных результатов. Давайте посмотрим, как работает тест с async/await.

      async/await

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

      Можно упростить наш тест saveToFile(), который основан на обещании с async/await. В вашем текстовом редакторе создайте эти незначительные изменения к тесту saveToFile() в index.test.js:

      todos/index.test.js

      ...
      describe("saveToFile()", function() {
          it("should save a single TODO", async function() {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Первое изменение — это тот факт, что функция, используемая it(), имеет ключевое слово async, когда она определена. Это позволяет использовать ключевое слово await в ее теле.

      Второе изменение обнаруживается, когда мы вызываем saveToFile(). Ключевое слово await используется перед вызовом. Теперь Node.js знает, что нужно ждать, пока эта функция не решится перед продолжением теста.

      Код функции проще читать, когда мы переместили код, который был в функции then() в тело функции it(). Запуск этого кода с помощью npm test дает следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO 3 passing (30ms)

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

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

      Шаг 5 — Использование хуков для улучшения тестовых случаев

      Хуки — полезный элемент Mocha, который позволяет нам настроить среду до и после теста. Обычно мы добавляем хуки в блок функции describe(), так как они содержат логику установки и сноса, специфичную для некоторых тестовых случаев.

      Mocha предоставляет четыре типа хуков, которые используются в тестах:

      • before: этот хук запускается один раз перед началом первого теста.
      • beforeEach: этот хук запускается перед каждым тестовым случаем.
      • after: этот хук запускается один раз после завершения последнего тестового случая.
      • afterEach: этот хук запускается после каждого тестового случая.

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

      Для просмотра значения хуков добавим больше тестов в наш блок теста saveToFile().

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

      Сначала добавим второй тест для подтверждения того, что наш файл сохранен корректно, после завершения элемента списка TODO. Откройте файл index.test.js в своем текстовом редакторе:

      Измените последний тест на следующее:

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          it("should save a single TODO", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              let todos = new Todos();
              todos.add("save a CSV");
              todos.complete("save a CSV");
              await todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Тест аналогичен тому, что мы делали ранее. Основное отличие в том, что мы вызываем функцию complete() перед вызовом saveToFile() и что ожидаемые элементы файла expectedFileContents​​​ теперь имеют значение true вместо false для столбца completed.

      Сохраните и закройте файл.

      Запустим наш новый тест, а также все остальные с помощью npm test​​​:

      В результате вы получите следующий вывод:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO ✓ should save a single TODO that's completed 4 passing (26ms)

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

      Сделаем эти улучшения с помощью тестовых хуков. Мы используем хук beforeEach() для настройки тестовой конфигурации элементов TODO. Тестовая конфигурация — это любое последовательное состояние, используемое в тесте. В нашем случае тестовая конфигурация — это новый объект todos, в который уже добавлен один элемент TODO. Затем мы используем afterEach() для удаления файла, созданного тестом.

      В index.test.js внесите следующие изменения в ваш последний тест для saveToFile():

      todos/index.test.js

      ...
      describe("saveToFile()", function () {
          beforeEach(function () {
              this.todos = new Todos();
              this.todos.add("save a CSV");
          });
      
          afterEach(function () {
              if (fs.existsSync("todos.csv")) {
                  fs.unlinkSync("todos.csv");
              }
          });
      
          it("should save a single TODO without error", async function () {
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync("todos.csv"), true);
              let expectedFileContents = "Title,Completednsave a CSV,falsen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      
          it("should save a single TODO that's completed", async function () {
              this.todos.complete("save a CSV");
              await this.todos.saveToFile();
      
              assert.strictEqual(fs.existsSync('todos.csv'), true);
              let expectedFileContents = "Title,Completednsave a CSV,truen";
              let content = fs.readFileSync("todos.csv").toString();
              assert.strictEqual(content, expectedFileContents);
          });
      });
      

      Давайте разберем все изменения, которые мы внесли. Мы добавили блок beforeEach() в тестовый блок:

      todos/index.test.js

      ...
      beforeEach(function () {
          this.todos = new Todos();
          this.todos.add("save a CSV");
      });
      ...
      

      Эти две строки кода создают новый объект Todos, доступный для каждого нашего теста. С Mocha объект this в beforeEach() относится к такому же объекту this в it(). this одинаков для каждого блока кода внутри блока describe(). Подробнее о this ищите в нашем обучающем руководстве Понимание методов This, Bind, Call и Apply в JavaScript​​​.

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

      Затем мы очищаем наш файл CSV в функции afterEach():

      todos/index.test.js

      ...
      afterEach(function () {
          if (fs.existsSync("todos.csv")) {
              fs.unlinkSync("todos.csv");
          }
      });
      ...
      

      Если тест не прошел, то он не мог создать файл. Поэтому мы проверяем наличие файла перед использованием функции unlinkSync() для его удаления.

      Остальные изменения переключают ссылку с объектов todos, созданных ранее в функции it()​​​, на this.todos, имеющиеся в контексте Mocha. Также мы удалили строки, которые ранее инстанциировали todos в отдельных тестовых случаях.

      Теперь запустим этот файл, чтобы убедиться, что тест все еще работает. Введите в терминале npm test​​​, чтобы получить следующее:

      Output

      ... integrated test ✓ should be able to add and complete TODOs complete() ✓ should fail if there are no TODOs saveToFile() ✓ should save a single TODO without error ✓ should save a single TODO that's completed 4 passing (20ms)

      Результаты те же, и к тому же мы немного сократили время установки для новых тестов для функции saveToFile() и нашли решение для остаточного файла CSV.

      Заключение

      В этом обучающем руководстве мы написали модуль Node.js для управления элементами TODO и протестировали код вручную с помощью Node.js REPL. Затем создали тестовый файл и использовали фреймворк Mocha для запуска автоматизированных тестов. С помощью модуля assert мы смогли проверить корректность работы кода. Также протестировали синхронные и асинхронные функции с Mocha. Наконец, создали хуки с Mocha, которые делают написание связанных тестовых случаев более читабельным и управляемым.

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

      Если вы хотите узнать больше о тестовом фреймворке Mocha, ознакомьтесь с официальной документацией Mocha. Если вы хотите продолжить изучение Node.js, вы можете перейти к странице серии Написание кода на Node.js.



      Source link

      Создание модуля Node.js


      Автор выбрал фонд Open Internet/Free Speech для получения пожертвования в рамках программы Write for DOnations.

      Введение

      В Node.js модуль — это набор функций и объектов JavaScript, который могут использовать внешние приложения. Описание части кода как модуля относится не столько к самому коду, сколько к тому, что он делает. Любой файл или набор файлов Node.js можно считать модулем, если его функции и данные готовы для использования внешними программами.

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

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

      Предварительные требования

      • Необходимо, чтобы в вашей среде разработки были установлены Node.js и npm. В этом обучающем руководстве используется версия 10.17.0. Чтобы установить его в macOS или Ubuntu 18.04, следуйте указаниям руководства Установка Node.js и создание локальной среды разработки в macOS или раздела Установка с помощью PPA руководства Установка Node.js в Ubuntu 18.04. При установке Node.js также выполняется установка npm, в этом обучающем руководстве используется версия 6.11.3.
      • Вы должны быть знакомы с файлом package.json, опыт с командами npm также будет полезен. Чтобы приобрести этот опыт, выполните обучающее руководство Использование модулей Node.js с npm и package.json, в особенности раздел Шаг 1 — Создание файла package.json.
      • Также будет полезно познакомиться со структурой Node.js REPL (чтение-оценка-печать-цикл). Мы используем это для тестирования нашего модуля. Если вам требуется дополнительная информация, обратитесь к руководству Использование REPL в Node.js.

      Шаг 1 — Создание модуля

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

      Для начала мы определим, какие данные о цветах будут храниться в вашем модуле. Каждый цвет будет представлять собой объект со свойством name, которое люди смогут легко распознать, и со свойством code, представляющим собой строку с цветовым кодом HTML. Цветовые коды HTML представляют собой шестизначные числа в шестнадцатеричном формате, позволяющие изменять цвет элементов веб-страницы. Дополнительную информацию о цветовых кодах HTML можно узнать в статье Цветовые коды и наименования в HTML.

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

      Откройте терминал, создайте новую папку colors и перейдите в нее:

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

      Мы используем флаг -y, чтобы пропустить обычные диалоги настройки файла package.json. Если бы вы публиковали этот модуль в npm, вы бы ввели в этих диалогах соответствующие данные, как объясняется в статье Использование модулей Node.js с npm и package.json.

      В данном случае вывод будет выглядеть так:

      Output

      { "name": "colors", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

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

      Ваш модуль будет выполнять несколько задач. Вначале вы определите класс Color. Экземпляр класса Color будет создаваться с именем и кодом HTML. Добавьте следующие строки для создания класса:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      

      Теперь у вас имеется структура данных для Color, и вы можете добавить в модуль несколько экземпляров. Запишите в файл следующий выделенный массив:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      

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

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      
      exports.getRandomColor = () => {
        return allColors[Math.floor(Math.random() * allColors.length)];
      }
      
      exports.allColors = allColors;
      

      Ключевое слово exports ссылается на глобальный объект, доступный в каждом модуле Node.js. Все функции и объекты, хранящиеся в объекте exports модуля, становятся открытыми, когда другие модули Node.js импортируют этот объект. Например, функция getRandomColor() была создана напрямую на объекте exports. Затем мы добавили свойство allColors в объекте exports. Это свойство ссылается на локальный постоянный массив allColors, созданный на предыдущих шагах сценария.

      При импорте этого модуля другими модулями allColors и getRandomColor() открываются и становятся доступными для использования.

      Сохраните и закройте файл.

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

      Шаг 2 — Тестирование модуля с REPL

      Прежде чем создать полное приложение, нужно убедиться, что наш модуль работает. На этом шаге мы используем REPL для загрузки модуля colors. В REPL вы вызовете функцию getRandomColor() и убедитесь, что она ведет себя ожидаемым образом.

      Запустите Node.js REPL в той же папке, что и файл index.js:

      После запуска REPL вы увидите командную строку >. Это означает, что вы можете ввести код JavaScript, который немедленно пройдет оценку. Если вы хотите узнать больше об этом, следуйте нашим указаниям по использованию REPL.

      Вначале введите следующее:

      • colors = require('./index');

      В этой команде require() загружает модуль colors в точке входа. При нажатии ENTER вы получите следующее:

      Output

      { getRandomColor: [Function], allColors: [ Color { name: 'brightred', code: '#E74C3C' }, Color { name: 'soothingpurple', code: '#9B59B6' }, Color { name: 'skyblue', code: '#5DADE2' }, Color { name: 'leafygreen', code: '#48C9B0' }, Color { name: 'sunkissedyellow', code: '#F4D03F' }, Color { name: 'groovygray', code: '#D7DBDD' } ] }

      REPL показывает нам значение colors, где содержатся все функции и объекты, импортированные из файла index.js. При использовании ключевого слова require Node.js возвращает все содержимое объекта exports нашего модуля.

      Если вы помните, мы добавили getRandomColor() и allColors в exports в модуле colors. Поэтому вы увидите их в REPL после импорта.

      Протестируйте функцию getRandomColor() в командной строке:

      Будет выведен случайный цвет:

      Output

      Color { name: 'groovygray', code: '#D7DBDD' }

      Поскольку индекс случайный, вывод может отличаться. Мы убедились в работе модуля colors и теперь можем выйти из Node.js REPL:

      Эта команда вернет вас в командную строку терминала.

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

      Шаг 3 — Сохранение локального модуля как зависимости

      При тестировании модуля REPL мы использовали для его импорта относительный путь. Это означает, что мы использовали расположение файла index.js по отношению к рабочей директории для получения ее содержимого. Хотя такой подход работает, с точки зрения программирования лучше импортировать модули по именам, чтобы импортированные модули не перестали работать при изменении контекста. На этом шаге мы установим модуль colors с помощью функции install локального модуля npm.

      Установите новый модуль Node.js вне папки colors. Вначале вернитесь в предыдущую директорию и содайте новую папку:

      • cd ..
      • mkdir really-large-application

      Теперь переходите к новому проекту:

      • cd really-large-application

      Как и в случае с модулем colors, инициализируйте папку с помощью npm:

      Будет сгенерирован следующий файл package.json:

      Output

      { "name": "really-large-application", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo "Error: no test specified" && exit 1" }, "keywords": [], "author": "", "license": "ISC" }

      Установите модуль colors и используйте флаг --save, чтобы он был записан в ваш файл package.json:

      • npm install --save ../colors

      Вы только что установили модуль colors в новый проект. Откройте файл package.json, чтобы посмотреть новую локальную зависимость:

      Вы увидите, что добавлены следующие выделенные строки:

      ~/really-large-application/package.json

      {
        "name": "really-large-application",
        "version": "1.0.0",
        "description": "",
        "main": "index.js",
        "scripts": {
          "test": "echo "Error: no test specified" && exit 1"
        },
        "keywords": [],
        "author": "",
        "license": "ISC",
        "dependencies": {
          "colors": "file:../colors"
        }
      }
      

      Закройте файл.

      Модуль colors был скопирован в вашу директорию node_modules. Используйте следующую команду, чтобы проверить его расположение:

      Результат будет выглядеть следующим образом:

      Output

      colors

      Используйте в новой программе установленный локальный модуль. Заново откройте текстовый редактор и создайте еще один файл JavaScript:

      Вначале ваша программа импортирует модуль colors. Затем она выберет случайный цвет с помощью функции getRandomColor()​​​, предоставленной модулем. В заключение она выведет на консоль сообщение, которое скажет пользователю, какой цвет использовать.

      Введите в index.js следующий код:

      ~/really-large-application/index.js

      const colors = require('colors');
      
      const chosenColor = colors.getRandomColor();
      console.log(`You should use ${chosenColor.name} on your website. It's HTML code is ${chosenColor.code}`);
      

      Сохраните и закройте файл.

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

      Запустите этот скрипт с помощью следующей команды:

      Вывод будет выглядеть следующим образом:

      Output

      You should use leafygreen on your website. It's HTML code is #48C9B0

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

      Шаг 4 — Привязка локального модуля

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

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

      Вначале следует удалить локальный модуль:

      npm привязывает модули, используя символические ссылки (symlink), представляющие собой указатели на файлы или директории вашего компьютера. Привязка модуля выполняется в два этапа:

      1. Создание глобальной ссылки на модуль. npm создает связь symlink между глобальной директорией node_modules и директорией вашего модуля. В глобальной директории node_modules устанавливаются все системные пакеты npm (любые пакеты, устанавливаемые с флагом -g).
      2. Создание локальной ссылки. npm создает связь symlink между локальным проектом, который использует модуль, и глобальной ссылкой модуля.

      Вначале нужно создать глобальную ссылку, вернувшись в папкуcolors и используя команду link:

      • cd ../colors
      • sudo npm link

      После этого в оболочке появится следующее:

      Output

      /usr/local/lib/node_modules/colors -> /home/sammy/colors

      Вы только что создали связь symlink между папкой node_modules и директорией colors.

      Теперь вернитесь в папку really-large-application и выполните привязку пакета:

      • cd ../really-large-application
      • sudo npm link colors

      Вы получите примерно следующий результат:

      Output

      /home/sammy/really-large-application/node_modules/colors -> /usr/local/lib/node_modules/colors -> /home/sammy/colors

      Примечание. Если вам нравится сокращать, вы можете использовать синтаксис ln вместо link. Например, команда npm ln colors будет работать точно так же.

      Как показывает вывод, вы только что создали связь symlink между локальной директорией node_modules приложения really-large-application и связью symlink директории colors в глобальной node_modules, которая указывает на фактическую директорию в модуле colors.

      Процесс привязки завершен. Запустите файл, чтобы проверить его работу:

      Вывод будет выглядеть следующим образом:

      Output

      You should use sunkissedyellow on your website. It's HTML code is #F4D03F

      Функционал вашей программы не пострадал. Протестируйте обновления и убедитесь, что они применяются немедленно. Заново откройте в текстовом редакторе файл index.js в модуле colors:

      • cd ../colors
      • nano index.js

      Теперь добавьте функцию, выбирающую лучший оттенок синего. Она не принимает аргументов и всегда возвращает третью позицию массива allColors. Добавьте эти строки в конец файла:

      ~/colors/index.js

      class Color {
        constructor(name, code) {
          this.name = name;
          this.code = code;
        }
      }
      
      const allColors = [
        new Color('brightred', '#E74C3C'),
        new Color('soothingpurple', '#9B59B6'),
        new Color('skyblue', '#5DADE2'),
        new Color('leafygreen', '#48C9B0'),
        new Color('sunkissedyellow', '#F4D03F'),
        new Color('groovygray', '#D7DBDD'),
      ];
      
      exports.getRandomColor = () => {
              return allColors[Math.floor(Math.random() * allColors.length)];
              }
      
      exports.allColors = allColors;
      
      exports.getBlue = () => {
        return allColors[2];
      }
      

      Сохраните и закройте файл, затем заново откройте файл index.js в папке really-large-application:

      • cd ../really-large-application
      • nano index.js

      Создайте вызов созданной функции getBlue() и распечатайте предложение со свойствами цвета. Добавьте эти выражения в конец файла:

      ~/really-large-application/index.js

      const colors = require('colors');
      
      const chosenColor = colors.getRandomColor();
      console.log(`You should use ${chosenColor.name} on your website. It's HTML code is ${chosenColor.code}`);
      
      const favoriteColor = colors.getBlue();
      console.log(`My favorite color is ${favoriteColor.name}/${favoriteColor.code}, btw`);
      

      Сохраните и закройте файл.

      Теперь в коде используется созданная функция getBlue(). Запустите файл как и раньше:

      Вы увидите примерно следующее:

      Output

      You should use brightred on your website. It's HTML code is #E74C3C My favorite color is skyblue/#5DADE2, btw

      Ваше приложение смогло использовать новую функцию модуля colors без запуска npm update. Это упрощает внесение изменений в приложение в процессе разработки.

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

      Заключение

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

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



      Source link