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

Введение

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

root
├── src
│   └── handlers
│   └── repositories
│   └── services
│   └── app.js
├── test
  1. Handler
    Handler или HTTP-обработчик — это функция, которая будет выполнять запрос при совпадении маршрута. Этот уровень отвечает за определение вывода, который будет возвращен клиенту.
  2. Служба
    Служба – это уровень, который контролирует бизнес-логику.
  3. Репозиторий
    Репозиторий — это уровень, который напрямую связан с базой данных или HTTP-вызовом, задача которого заключается в управлении данными.

Подготовка

Создайте корень проекта и установите необходимые модули. Для модуля HTTP-запросов мы используем axios, express для маршрутизации, а для модульного тестирования у нас есть mocha, chai, nock и sinon.

npm install --save axios express
npm install --save-dev mocha chai nock sinon

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

Мокко

Mocha — это тестовый фреймворк JavaScript, то есть это инструмент, который выполняет наши тесты. Другие популярные тестовые фреймворки, такие как Jest и Jasmine. Вот пример использования мокко.

it("first testing", function () {
  // your code
});
it("second testing", function () {
  // your code
});

Чтобы запустить тест mocha, вы можете запустить двоичный файл mocha внутри node_modules.

./node_modules/mocha/bin/mocha

Если вы устанавливаете его глобально, вам нужно только выполнить mocha.

Чай

Chai — это библиотека утверждений. Мы используем это, чтобы утверждать, что следует делать при тестировании. Например, мы ожидали, что функция должна вернуть 1.

const expect = chai.expect;
expect(foo).to.equal(1);

Нок

Nock — библиотека для имитации HTTP-сервера. Nock можно использовать для тестирования модулей, которые делают HTTP-запросы отдельно. Вот пример того, как мы использовали nock для имитации HTTP-запроса.

const scope = nock("http://www.example.com")
  .get("/resource")
  .reply(200, "your-mock-data")

СинонJS

SinonJS — это библиотека, которая предоставляет автономные тестовые шпионы, заглушки и макеты. Хм, какие они? Итак, в основном SinonJS имеет три основные функции.

  1. Шпионы
    Шпионы создают поддельные функции, которые мы можем использовать для отслеживания казней. Это означает, что мы можем узнать, вызывалась ли функция или сколько раз функция выполнялась.
  2. Заглушки
    Заглушки управляют поведением метода, что означает, что вы можете управлять тем, что возвращается, или вызывать ошибку.
  3. Моки
    Моки определяют, как мы ожидаем, что функция будет работать, и используют mock.verify(), чтобы убедиться, что она работает так, как мы ожидаем.

В этой статье я не буду использовать функцию mocks. Если вы хотите изучить его, вы можете посетить https://sinonjs.org/releases/latest/mocks/.

Давай попрактикуемся

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

Сначала я создал репозиторий компании. В этом репозитории я извлек данные с помощью вызовов HTTP.

/src/repositories/company.repository.js

Во-вторых, я создал сервис компании. В этом сервисе я подобрал id и name пользователя и сотрудника и слил в один и тот же объект.

/src/services/company.service.js

Наконец, это обработчик компании. Ничего особенного, я просто вернул данные профиля компании в виде JSON.

/src/handlers/company.handler.js

Это наша иерархия папок.

root
├── src
│   ├── handlers
│   │   └── company.handler.js
│   ├── repositories
│   │   └── company.repository.js
│   ├── services
│   │   └── company.service.js
│   └── app.js
├── test

Время тестирования

Итак, теперь мы собираемся протестировать эти слои, давайте создадим папку с именем test, а затем создадим тестовый файл для каждого слоя. Я также создал два файла для хранения фиктивных данных пользователя и сотрудника.

root
├── src
├── test
    ├── mocks
    │   └── employees.js
    │   └── users.js
    ├── company.handler.test.js
    ├── company.service.test.js
    └── company.repository.test.js

/test/mocks/users.js

module.exports = [
  {
    "login": "mojombo",
    "id": 1,
    "node_id": "MDQ6VXNlcjE=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/1?v=4",
    "gravatar_id": "",
    "type": "User",
    "site_admin": false
  },
  {
    "login": "defunkt",
    "id": 2,
    "node_id": "MDQ6VXNlcjI=",
    "avatar_url": "https://avatars0.githubusercontent.com/u/2?v=4",
    "gravatar_id": "",
    "type": "User",
    "site_admin": false
  }
];

/test/mocks/employees.js

module.exports = {
  "status": "success",
  "data": [
    {
      "id": "1",
      "employee_name": "Tiger Nixon",
      "employee_salary": "320800",
      "employee_age": "61",
      "profile_image": ""
    },
    {
      "id": "2",
      "employee_name": "Garrett Winters",
      "employee_salary": "170750",
      "employee_age": "63",
      "profile_image": ""
    }
  ]
};

Протестируйте репозиторий

В репозитории мы создали метод fetchUsers для получения списка пользователей по HTTP-вызову.

async function fetchUsers() {
  const { data } = 
    await axios.get("https://api.github.com/users");  
  
  return data;
}

Как мы это проверим? Вот как я тестирую репозиторий.

/test/company.repository.test.js

При тестировании репозитория мы реализуем nock для имитации HTTP-запроса, это означает, что мы берем под контроль возвращаемый HTTP-запрос.

it("should succeed", async function () {
  const usersScope = nock("https://api.github.com")
    .get("/users")        
    .reply(
      200, 
      usersMock, 
      { 'Content-Type': 'application/json' }
    );
  ...
});

Мы указываем, что эта конечная точка должна возвращать статус 200, что означает успех, и ответ usersMock (/test/mocks/users.js).

Затем мы ожидаем, что должен вернуть fetchUsers. usersScope.done() используется для выдачи ошибки утверждения, если при этом HTTP-запрос не был выполнен.

it("should succeed", async function () {
  ...
  const users = await companyRepository.fetchUsers();
  expect(users).to.have.lengthOf(2);
  expect(users).to.deep.equal(usersMock);
  expect(users[0].login).to.equal("mojombo");
  usersScope.done();
});

Протестируйте сервис

В сервисе мы делаем простую обработку данных.

...
async function fetchCompanyProfile() {
  const users = await companyRepository.fetchUsers();
  const employees = await companyRepository.fetchEmployees();
  
  const companyUsers = [];
  for (let user of users) {
    companyUsers.push({
      id: user.id,
      name: user.login,
    });
  }
  const companyEmployees = [];
  for (let employee of employees) {
    companyEmployees.push({
      id: parseInt(employee.id),
      name: employee.employee_name,
    });
  }
  return {
    companyName: "Facebook Inc",
    companyUsers,
    companyEmployees,
  };
}
...

Как мы это проверим? Вот как я тестирую сервис.

/test/company.service.test.js

В этом тестировании мы реализуем sinon.stub для имитации функций fetchUsers и fetchEmployees.

describe("test fetch company profile", function () {
  it("should succeed", async function () {
  const fetchUsersFn = stub(companyRepository, "fetchUsers")
    .returns(usersMock);
  const fetchEmployeesFn = stub(
      companyRepository, 
      "fetchEmployees"
    ).returns(employeesMock.data);
  ...

Функция fetchUsers возвращает usersMock (/test/mocks/users.js), а fetchEmployees возвращает employeesMock.data (/test/mocks/employees.js).

Вот что мы ожидаем, что fetchCompanyProfile будет возвращено, и ожидается, что fetchUsers и fetchEmployees будут вызваны.

...
expect(fetchUsersFn.calledOnce).to.be.true;      expect(fetchEmployeesFn.calledOnce).to.be.true;
const expectedData = {
  companyName: 'Facebook Inc',
  companyUsers: [
    { id: 1, name: 'mojombo' },
    { id: 2, name: 'defunkt' },
  ],
  companyEmployees: [
    { id: 1, name: 'Tiger Nixon' },
    { id: 2, name: 'Garrett Winters' },
  ],
};       
expect(companyProfile).to.deep.equal(expectedData);

Протестируйте обработчик

В функции-обработчике мы возвращаем данные только в формате JSON для клиента.

...
async function fetchCompanyProfile(req, res) {
  try {
    const companyProfile = await companyService.fetchCompanyProfile();
    
    res.json(companyProfile);
  } catch (e) {
    errorResponse(e, res);
  }
}
...

Это наш тест обработчика.

/test/company.handler.test.js

Мы делаем что-то похожее на служебный тест, имитируя функцию fetchCompanyProfile с помощью stub. Проблема здесь в том, как я издеваюсь над запросом и ответом с помощью spy. Поскольку мы используем res.json() для возврата ответа JSON клиенту, нам нужно только заменить реальную функцию на sinon.spy(). Spy на самом деле не дает нам контроля над функцией, он только дает нам фиктивную функцию, которую мы можем использовать для отслеживания выполнения функции.

const req = {};
const res = {
  json: spy(),
};
await companyHandler.fetchCompanyProfile(req, res);
expect(fetchCompanyProfileFn.calledOnce).to.be.true;    expect(res.json.calledOnce).to.be.true;    expect(res.json.firstCall.args[0]).to.deep
  .equal(companyProfileMock);

Запустить тест

Чтобы запустить тест, вы можете установить команду test в поле scripts в package.json, вызвав двоичный файл mocha из node_modules.

/package.json

"scripts": {
  "test": "./node_modules/mocha/bin/mocha"
},

После этого запустите npm run test в командной строке.

➜  jstesting git:(master) ✗ npm run test
> [email protected] test /Users/rezaindra/Desktop/nodecode/KurioApp/jstesting
> ./node_modules/mocha/bin/mocha
test company handler
    ✓ should succeed
    ✓ should be error
test company repository
    test fetch users
      ✓ should succeed
      ✓ should be error
    test fetch employees
      ✓ should succeed
      ✓ should be error
test company service
    test fetch company profile
      ✓ should succeed
      ✓ should be error when fetching users
      ✓ should be error when fetching employees
9 passing (44ms)

Вывод

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

Удачного тестирования!