Знакомство с разработкой через тестирование в JavaScript

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

Что должно уметь наше приложение:

  • Складывать, вычитать, делить и умножать любые два числа;
  • Показывать предупреждение и завершать работу, если было введено что-то отличное от числа;
  • Также должен быть интерфейс командной строки, чтобы конечный пользователь мог воспользоваться приложением.

Что нам потребуется:

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

Настраиваем среду

Так как мы используем Node.js, нужно создать локальное окружение для файлов и зависимостей.

Создайте новую папку calc. В командной строке перейдите в эту директорию и создайте новый проект командой npm init, которая создаст новый файл package.json для нашей программы.

Вам предложат ввести имя пакета, версию, описание и прочую информацию о пакете. Вы можете ввести имя calc.js и дальше жать Enter для присвоения значений по умолчанию. Когда вы дойдёте до test command, введите mocha — это фреймворк для тестирования, который мы будем использовать:

test command: mocha

После ввода всей информации скрипт создаст файл package.json, который выглядит примерно так:

{
  "name": "calc.js",
  "version": "1.0.0",
  "description": "Простой калькулятор на Node.js",
  "main": "index.js",
  "scripts": {
    "test": "mocha"
  },
  "author": "",
  "license": "ISC"
}

Последний шаг на данном этапе — установка Mocha. Введите следующую команду для установки:

npm install --save-dev mocha

После применения этой команды появится папка node_modules, файл package-lock.json, а в файле package.json появятся следующие строки:

"devDependencies": {
    "mocha": "^4.0.1"
}

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

Создайте файл test.js. Мы воспользуемся встроенным в Node.js модулем assert, чтобы проверить верность равенства true и true. Так как оно верно, тест должен пройти успешно:

const assert = require('assert');

it('должно возвращать true', () => {
  assert.equal(true, true);
}); 

Теперь запустите тест из командной строки:

$ npm test
> mocha

  ✓ должно возвращать true
  1 passing (8ms)

Тест прошёл как и ожидалось, поэтому с настройкой среды покончено. Удалите из test.js всё, кроме строки const assert = require('assert');.

Мы будем использовать файл test.js на протяжении всего процесса создания приложения. Создайте ещё два файла: operations.js для арифметических и валидационных функций и calc.js для самого приложения. Мы используем так много файлов, чтобы они не становились слишком длинными и сложными. Вот наш текущий список файлов:

  • calc.js;
  • node_modules;
  • operations.js;
  • package-lock.json;
  • package.json;
  • test.js;

Давайте добавим первый настоящий тест для нашего приложения.

Добавляем математические операции

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

Начнём со сложения. Мы напишем тест, в котором однозначно получится ожидаемая сумма двух чисел. В коде ниже мы проверяем, равняется ли сумма 1 и 3 с помощью функции add() 4:

const assert = require('assert');

it('правильно находит сумму 1 и 3', () => {
  assert.equal(add(1, 3), 4);
});

После запуска теста с помощью команды npm test мы видим следующее:

> mocha 

  0 passing (9ms)
  1 failing

  1) правильно находит сумму 1 и 3:
     ReferenceError: add is not defined
     at Context.it (test.js:5:16)

npm ERR! Test failed.  See above for more details.

Тест провалился с сообщением ReferenceError: add is not defined. Мы тестируем функцию add(), которой ещё нет, поэтому такой результат вполне ожидаем.

Создадим функцию add() в файле operations.js:

const add = (x, y) => (+x) + (+y);

Эта функция принимает два аргумента x и y и возвращает их сумму. Вы могли заметить, что мы пишем (+x) + (+y), а не x + y. Мы используем унарный оператор для приведения аргумента к числу, на случай, если ввод будет строкой.

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

Так как мы используем Node.js и разбиваем код на множество файлов, нужно воспользоваться module.exports, чтобы экспортировать код:

const add = (x, y) => (+x) + (+y);

module.exports = { add }

В начале файла test.js мы импортируем код из operations.js с помощью require(). Так как мы используем функцию через переменную operations, нужно поменять add() на operations.add():

const operations = require('./operations.js');
const assert = require('assert');

it('правильно находит сумму 1 и 3', () => {
  assert.equal(operations.add(1, 3), 4);
});

Запускаем тест:

$ npm test

> mocha

  ✓ правильно находит сумму 1 и 3

  1 passing (8ms)

Теперь у нас есть работающая функция, и тесты проходят успешно. Так как функции других операций работают схожим образом, добавить тесты для subtract()multiply() и divide() не составит труда:

it('правильно находит сумму 1 и 3', () => {
  assert.equal(operations.add(1, 3), 4);
});

it('правильно находит сумму -1 и -1', () => {
  assert.equal(operations.add(-1, -1), -2);
});

it('правильно находит разность 33 и 3', () => {
  assert.equal(operations.subtract(33, 3), 30);
});

it('правильно находит произведение 12 и 12', () => {
  assert.equal(operations.multiply(12, 12), 144);
});

it('правильно находит частное 10 и 2', () => { 
  assert.equal(operations.divide(10, 2), 5);
});

Теперь создадим и экспортируем все функции в test.js:

const add = (x, y) => (+x) + (+y);
const subtract = (x, y) => (+x) - (+y);
const multiply = (x, y) => (+x) * (+y);
const divide = (x, y) => (+x) / (+y);

module.exports = {
  add,
  subtract,
  multiply,
  divide,
}

И запустим новые тесты:

$ npm test

> mocha

  ✓ правильно находит сумму 1 и 3
  ✓ правильно находит сумму -1 и -1
  ✓ правильно находит разность 33 и 3
  ✓ правильно находит произведение 12 и 12
  ✓ правильно находит частное 10 и 2

  5 passing (8ms)

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

Добавляем валидацию

На данный момент, когда пользователь вводит число и выбирает нужную операцию, всё работает нормально. Однако что случится, если попытаться найти сумму числа и строки? Приложение попытается выполнить операцию, но из-за того, что оно ожидает числа, оно вернёт NaN.

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

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

  1. Оба ввода — числа.
  2. Один ввод — число, а другой — строка.
  3. Оба ввода — строки.
it('сообщает об ошибке при использовании строки вместо числа', () => {
  assert.equal(operations.validateNumbers('sammy', 5), false);
});

it('сообщает об ошибке при использовании двух строк вместо чисел', () => {
  assert.equal(operations.validateNumbers('sammy', 'sammy'), false);
});

it('успех при использовании двух чисел', () => {
  assert.equal(operations.validateNumbers(5, 5), true);
});

Функция validateNumbers() будет проверять оба параметра. Функция isNaN() проверяет, не является ли параметр числом, и если нет, то возвращает false. В противном случае она возвращает true, что означает успешную валидацию.

const validateNumbers = (x, y) => {
  if (isNaN(x) && isNaN(y)) {
    return false;
  }
  return true;
}

Не забудьте добавить validateNumbers в module.exports в конце файла. Теперь можно запускать новые тесты:

$ npm test

  1) сообщает об ошибке при использовании строки вместо числа
  ✓ сообщает об ошибке при использовании двух строк вместо чисел
  ✓ успех при использовании двух чисел

  7 passing (12ms)
  1 failing


  1) сообщает об ошибке при использовании строки вместо числа:

      AssertionError [ERR_ASSERTION]: true == false
      + expected - actual

      -true
      +false

Два теста прошли, но один провалился. Проверка на ввод двух чисел прошла успешно, так же как и проверка на ввод двух строк. Чего нельзя сказать о проверке на ввод строки и числа.

Если взглянуть на нашу функцию ещё раз, то можно заметить, что оба параметра должны быть NaN, чтобы функция вернула false. Если мы хотим добиться того же эффекта, когда хотя бы один из параметров равен NaN, нужно заменить && на ||:

const validateNumbers = (x, y) => {
  if (isNaN(x) || isNaN(y)) {
    return false;
  }
  return true;
}

Если после этих изменений снова запустить npm test, то все тесты пройдут успешно:

 ✓ сообщает об ошибке при использовании строки вместо числа
 ✓ сообщает об ошибке при использовании двух строк вместо чисел
 ✓ успех при использовании двух чисел

 8 passing (9ms)

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

Создаём интерфейс

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

На данный момент файл calc.js должен быть пуст. Здесь и будет храниться наше приложение. Сначала нужно импортировать функции из operations.js:

const operations = require('./operations.js');

Сам интерфейс будет использовать встроенный в Node.js CLI-модуль Readline:

const readline = require('readline');

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

const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

Первое, что пользователь должен видеть после запуска программы, — приветственное сообщение и инструкции по использованию. Для этого мы воспользуемся console.log():

console.log(`
Calc.js

Вы открыли калькулятор на Node.js! 
Версия: 1.0.0.

Использование: пользователь должен ввести два числа, а затем
выбрать, что с ними сделать.
`);

Прежде чем мы займёмся самими функциями калькулятора, давайте проверим, что console.log() работает как надо. Мы сделаем так, чтобы программа выводила сообщение и завершала работу. Для этого добавьте в конце вызов метода rl.close().

Чтобы запустить приложение, введите node и имя файла:

$ node calc.js

Calc.js

Вы открыли калькулятор на Node.js! 
Версия: 1.0.0.

Использование: пользователь должен ввести два числа, а затем
выбрать, что с ними сделать.

Программа выводит приветственное сообщение и завершает свою работу. Теперь нужно добавить пользовательский ввод. От пользователя требуется следующее: выбрать два числа и одну операцию. Каждый ввод будет запрашиваться методом rl.question():

rl.question('Введите первое число: ', (x) => {
  rl.question('Введите второе число: ', (y) => {
    rl.question(`
Выберите одну из следующих операций:

[1] Сложение (+)
[2] Вычитание (-)
[3] Умножение (*)
[4] Деление (/)

Ваш выбор: `, (choice) => {
      // здесь ещё появится код
      rl.close();
    });
  });
});

Переменной x присваивается первое число, y — второе, а choice — выбранная операция. Теперь наша программа запрашивает ввод, но ничего не делает с полученными данными.

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

if (!operations.validateNumbers(x, y)) {
  console.log('Можно вводить только числа! Пожалуйста, перезапустите программу.');
}

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

if (!operations.validateNumbers(x, y)) {
  console.log('Можно вводить только числа! Пожалуйста, перезапустите программу.');
} else {
  switch (choice) {
    case '1':
      console.log(`Сумма ${x} и ${y} равна ${operations.add(x, y)}.`);
      break;
    case '2':
      console.log(`Разность ${x} и ${y} равна ${operations.subtract(x, y)}.`);
      break;
    case '3':
      console.log(`Произведение ${x} и ${y} равно ${operations.multiply(x, y)}.`);
      break;
    case '4':
      console.log(`Частное ${x} и ${y} равно ${operations.divide(x, y)}.`);
      break;
    default:
      console.log('Пожалуйста, перезапустите программу и выберите число от 1 до 4.');
      break;
  }
}

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

/**
 * Простой калькулятор на Node.js, который использует calculator app that uses
 * встроенный интерфейс командной строки Readline.
 */

const operations = require('./operations.js');
const readline = require('readline');

// Используем readline для создания интерфейса
const rl = readline.createInterface({
  input: process.stdin,
  output: process.stdout
});

console.log(`
Calc.js
Вы открыли калькулятор на Node.js! 
Версия: 1.0.0.
Использование: пользователь должен ввести два числа, а затем
выбрать, что с ними сделать.
`);

rl.question('Введите первое число: ', (x) => {
  rl.question('Введите второе число: ', (y) => {
    rl.question(`
Выберите одну из следующих операций:
[1] Сложение (+)
[2] Вычитание (-)
[3] Умножение (*)
[4] Деление (/)
Ваш выбор: `, (choice) => {
      if (!operations.validateNumbers(x, y)) {
        console.log('Можно вводить только числа! Пожалуйста, перезапустите программу.');
      } else {
        switch (choice) {
          case '1':
            console.log(`Сумма ${x} и ${y} равна ${operations.add(x, y)}.`);
            break;
          case '2':
            console.log(`Разность ${x} и ${y} равна ${operations.subtract(x, y)}.`);
            break;
          case '3':
            console.log(`Произведение ${x} и ${y} равно ${operations.multiply(x, y)}.`);
            break;
          case '4':
            console.log(`Частное ${x} и ${y} равно ${operations.divide(x, y)}.`);
            break;
          default:
            console.log('Пожалуйста, перезапустите программу и выберите число от 1 до 4.');
            break;
        }
      }
      rl.close();
    });
  });
});

Теперь наше приложение готово. Проверим его работу напоследок. Введём 999 и 1 и выберем операцию вычитания:

$ node calc.js
Введите первое число: 999
Введите второе число: 1
Ваш выбор: 2

Разность 999 и 1 равна 998.

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

Исходный код приложения доступен на GitHub.

Перевод статьи «Unit Testing in JavaScript»

Ещё интересное для вас:
— Биты, байты, Ада Лавлейс — тест на знание околоIT.
— Level Up — события и курсы, на которых можно поднять свой уровень.
— Работа мечты — лучшие IT-вакансии для вас.