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

Следующее доступно для скачивания.

Модульные тесты

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

В данной статье не стоит углубляться в это, но, согласно статье Мартин Фаулер, мы должны:

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

Надеюсь, я покажу, насколько легко написать экспресс-API с использованием TDD, и выйду с приложением, полностью протестированным с помощью классной тестовой среды: jest.

Приложение

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

Я определил действительно стандартную архитектуру с четырьмя файлами: индексом, файлом маршрутов, контроллером и службой.

Зависимости хорошо известны: экспресс, аксиомы и шутки.

индексный файл

Сначала давайте протестируем индексный файл. Назначение этого файла - инициализировать и настроить экспресс-приложение.

Мы знаем, что после инициализации экспресс-приложения оно предоставляет некоторые функции, такие как использование, прослушивание, получение и т. Д. Итак, мы должны сообщить шутке, что мы хотим имитировать экспресс:

const useSpy = jest.fn();
const listenSpy = jest.fn();
jest.doMock('express', () => {
  return () => ({ 
    listen: listenSpy, 
    use: useSpy 
  });
});

и тест:

test(’should initialize an express server’, () => {
  require(‘./index’);
  expect(listenSpy).toHaveBeenCalled();
});

Кроме того, мы хотим проверить, использует ли приложение промежуточное ПО и маршруты. В этом случае промежуточного ПО нет, но есть маршрутизатор с одной конечной точкой:

test(’should use a router, () => {
  require(‘./index’);
  expect(useSpy).toHaveBeenCalledWith(router);
});

И это почти все, очень просто. Ниже представлены два файла:

index.js

const express = require('express');
const app = express();
const router = require('./src/router');

app.use(router);
app.listen(3000, () => { console.log('Running') });

index.test.js

const router = require('./src/router');

const useSpy = jest.fn();
const listenSpy = jest.fn();

jest.doMock('express', () => {
  return () => ({
    use: useSpy,
    listen: listenSpy,
  })
});

describe('should test server configuration', () => {
  test('use router', () => {
    require('./index.js');
    expect(useSpy).toHaveBeenCalledWith(router);
  });

  test('should call listen fn', () => {
    require('./index.js');
    expect(listenSpy).toHaveBeenCalled();
  });
});

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

Файл маршрутов

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

const getSpy = jest.fn();
jest.doMock('express', () => {
  return {
    Router() {
      return {
        get: getSpy,
      }
    }
  }
});

и тест:

test('should test get POSTS', () => {
  expect(getSpy).toHaveBeenCalledWith('/posts', Controller.getPosts);
});

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

router.test.js

const Controller = require('./controller');

const getSpy = jest.fn();

jest.doMock('express', () => {
  return {
    Router() {
      return {
        get: getSpy,
      }
    }
  }
});

describe('should test router', () => {
  require('./router.js');
  test('should test get POSTS', () => {
    expect(getSpy).toHaveBeenCalledWith(1, '/posts', Controller.getPosts);
  });
});

router.js

const express = require('express');
const router = express.Router();
const Controller = require('./controller');

router.get('/posts', Controller.getPosts);

module.exports = router;

Контроллер

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

jest.mock('./service');

затем мы можем написать собственную реализацию для метода list службы. В этом случае мы используем jest-функцию mockImplementation и даем функцию, которая разрешается с обещанием сообщений:

const posts = [{}, {}];
Service.list.mockImplementation(() => Promise.resolve(posts));

Еще нужно помнить, что express выполняет функции контроллера с тремя параметрами: req, res и next.

Поскольку эти функции вызываем мы, нам нужно сделать макеты req, res и next. В этом случае для нашего теста будет достаточно мока res. Обратите внимание, что каждая функция res: status и json возвращает один и тот же объект, то есть имитирует поведение экспресс-цепочки:

const mockResponse = () => {
  const res = {};
  res.status = jest.fn().mockReturnValue(res);
  res.json = jest.fn().mockReturnValue(res);
  return res;
};
const res = mockResponse();
await Controller.getPosts(null, res, null);

наконец, этап утверждения:

expect(res.json).toBeCalledWith(posts);
expect(res.status).toBeCalledWith(201);

controller.test.js

test('should test #getPosts', async () => {
  const posts = [{}, {}];
  Service.list.mockImplementation(() => Promise.resolve(posts));
  const res = mockResponse();
  await Controller.getPosts(null, res, null);
  expect(res.json).toBeCalledWith(posts);
  expect(res.status).toBeCalledWith(201);
});

controller.js

const Service = require('./service');

const Controller = {
  getPosts(req, res, next) {
    return Service.list()
      .then((posts) => {
        return res.status(201).json(posts);
      })
      .catch((error) => {
        return next(error);
      });
  },
};

module.exports = Controller;

Служба

Последнее, что нужно протестировать, - это сервис. Основная задача - вызвать внешние сервисы. В данном случае jsonplaceholder.

Я предпочитаю делать запросы с помощью axios, чтобы не выполнять запрос, нам нужно его имитировать.

jest.mock('axios');

теперь мы можем протестировать метод list в случае успешного ответа на jsonPlaceholder:

test('list method should succeed', async () => {
  const response = { data: [{}, {}] };
  axios.get.mockImplementation(() => Promise.resolve(response));
  const actual = await Service.list();
  expect(actual).toEqual(response.data);
});

когда ответ не работает:

test('list method should fail', async () => {
  const error = new Error();
  axios.get.mockImplementation(() => Promise.reject(error));
  const actual = await Service.list();
  expect(actual).toEqual(error);
});

service.test.js

const axios = require('axios/index');
const Service = require('./service');

jest.mock('axios');

describe('should test Service', () => {
  test('when #list method succeeds', async () => {
    const response = { data: [{}, {}] };
    axios.get.mockImplementation(() => Promise.resolve(response));
    const actual = await Service.list();
    expect(actual).toEqual(response.data);
  });
  test('when #list method fails', async () => {
    const error = new Error();
    axios.get.mockImplementation(() => Promise.reject(error));
    const actual = await Service.list();
    expect(actual).toEqual(error);
  });
});

service.js

const axios = require('axios/index');

const Service = {
  basePath: 'https://jsonplaceholder.typicode.com/posts',

  list() {
    return axios.get(this.basePath)
      .then((response) => {
        return response.data;
      })
      .catch((error) => {
        return error;
      });
  },
};

module.exports = Service;

Заключение

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

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