Статьи

Тестируемые API с Node.js

API — это сердце современного веб-приложения. Это все для того, чтобы его было легко потреблять, масштабировать и убедиться, что он работает как положено. В настоящее время я придерживаюсь принципа «все методы открытого API должны иметь тесты» (AOAMMHT). Раньше я работал с технологиями .NET, где тестирование API было связано с вызовом методов соответствующего объекта контроллера, который обычно представлял собой модульное тестирование — макетирование всех зависимостей контроллера, настройка ожидаемых возвращаемых значений.

Я передумал на тестировании с разработкой Node.js / Express.js. Для API я предпочитаю «сквозное» тестирование: настройка учетной записи пользователя, аутентификация, HTTP-вызовы к серверу, реальные вызовы в БД и возврат полезной нагрузки JSON. API должны быть протестированы с точки зрения потребителей, чтобы иметь возможность получить значимые результаты.

Инструменты и рамки

Довольно стандартная настройка: мокко , чай , запрос .

Mocha — проверенный временем инструмент для тестирования приложений Node.js, Chai — достаточно хорошая среда ожидания и Request как один из лучших HTTP-клиентов, с которыми я когда-либо работал.

Подготовить заявку к тестированию

Вышеуказанные зависимости устанавливаются через npm installи должны быть сохранены в package.jsonфайл (с --saveопцией). В вашей структуре проекта должна быть testпапка внутри, которую мокко использует по умолчанию для просмотра тестов внутри. Там нужно добавить несколько файлов, у меня есть текущая структура, которая работает.

/test
  /specs
      auth.spec.js
      ...
  common.js
  mocha.opts
  utils.js
  runMocha.js

/apiПапка — это та, которая будет содержать спецификации для вашего API, mocha.opts— содержит глобальную конфигурацию mocha, common.jsявляется общим файлом require, который используют все тесты, utils.js— test helper, который будет содержать все необходимое во время тестирования, runMocha.jsутилиту, которая будет точкой входа для тестов.

mocha.opts

--require ./test/common.js
--reporter spec
--ui bdd
--recursive
--colors
--timeout 60000
--slow 300

Опции Mocha позволяют требовать некоторый дополнительный файл javascript, а также настраивать глобальные настройки Mocha, какой репортер использовать, время ожидания и т. Д.

common.js

global.chai = require('chai');
global.expect = global.chai.expect;

Чтобы не требовать chaiв каждом файле спецификаций, можно потребовать его один раз и поместить в глобальную область видимости.

runMocha.js

process.env.NODE_ENV = process.env.NODE_ENV || 'test';
process.env.TEST_ENV = process.env.TEST_ENV || 'test';

var exit = process.exit;
process.exit = function (code) {
  setTimeout(function () {
      exit();
  }, 200);
};

require('../source/server');
require('../node_modules/mocha/bin/_mocha');

Секретный соус — последние 2 строки. Поскольку requireсинхронно, мы сначала «вызываем» API-сервер, чтобы встать, а после этого «вызываем» механизм мокко, чтобы начать тестирование. Таким образом, внутри каждого теста мы можем выполнять реальные HTTP-вызовы на реальных HTTP-серверах. Никаких издевательств.

package.json

// ...
"scripts": {
    "test": "node test/runMocha",
  },
// ...

Файл пакета должен содержать скрипт для вызова тестов API с помощью простой npm testкоманды.

Тест управляемый API

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

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

var request = require('request');
var testUtils = require('../utils');

describe('collections.spec.js', function () {
  describe('when non authorized', function () {
      it ('should not be authorized', function () {
      });
  });

  describe('when authorized', function () {
      describe('when new collection created', function () {
          describe('public', function () {
              it('should respond with 201 (created)', function () {
              });

              it('should create new collection', function () {
              });

              it('should have user', function () {
              });

              it('should collection be public', function () {
              });

              describe('and title is missing', function () {
                  it('should respond with 412 (bad request)', function () {
                  });
              });

              describe('with description', function () {
                  it('should respond with 201 (created)', function () {
                  });

                  it('should create new collection', function () {
                  });
              });
          });
      });

      // etc..
  });
});

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

Не авторизованный доступ

Если ваш API или его часть требует авторизации, я предпочитаю протестировать его.

var token, user, url, headers, response, results;

beforeEach(function () {
  url = testUtils.getRootUrl() + '/api/collections';
});

describe('when non authorized', function () {
  beforeEach(function (done) {
      request.get({url: url, json: true}, function (err, resp, body) {
          response = resp;
          done(err);
      });
  });

  it ('should not be authorized', function () {
      expect(response.statusCode).to.equal(401);
  });
});

testUtils.getRootUrl()возвращает квалифицированный URL для API, в зависимости от среды тестирования. Во время разработки, это именно то, http://localhost:3000где вы server.jsначали.

Авторизованный доступ

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

Все тесты, требующие авторизации доступа, должны иметь такие beforeEach(),

beforeEach(function (done) {
  testUtils.createTestUserAndLoginToApi(function (err, createdUser, accessToken) {
      token = accessToken;
      user = createdUser;
      headers = {'X-Access-Token': accessToken};
      done(err);
  });
});

После получения access_tokenон может использоваться как часть любых авторизованных вызовов.

Поведенческие тесты

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

describe('when new collection created', function () {

  describe('public', function () {
      beforeEach(function () {
          collection = {title: 'This is test collection', public: true};
      });

      beforeEach(function (done) {
          request.post({url: url, headers: headers, body: collection, json: true}, function (err, resp, body) {
              response = resp;
              results = body;
              done(err);
          });
      });

      it('should respond with 201 (created)', function () {
          expect(response.statusCode).to.equal(201);
      });

      it('should create new collection', function () {
          expect(results.title).to.be.ok;
          expect(results._id).to.be.ok;
      });

      it('should have user', function () {
          expect(results.user).to.equal(user.email);
      });

      it('should collection be public', function () {
          expect(results.public).to.equal(true);
      });

      describe('and title is missing', function () {
          beforeEach(function (done) {
              request.post({url: url, headers: headers, body: {}, json: true}, function (err, resp, body) {
                  response = resp;
                  results = body;
                  done(err);
              });
          });

          it('should respond with 412 (bad request)', function () {
              expect(response.statusCode).to.equal(412);
          });
      });

      describe('with description', function () {
          beforeEach(function () {
              collection = {title: 'This is test collection', description: 'description'};
          });

          beforeEach(function (done) {
              request.post({url: url, headers: headers, body: collection, json: true}, function (err, resp, body) {
                  response = resp;
                  results = body;
                  done(err);
              });
          });

          it('should respond with 201 (created)', function () {
              expect(response.statusCode).to.equal(201);
          });

          it('should create new collection', function () {
              expect(results.description).to.equal('description');
          });
      });
  });
});

Выводы

Я думаю, что динамические языки, такие как JavaScript, отлично подходят для тестирования API. Отсутствие типов исключает классы «модель-на-ответ», request.jsотлично подходит для выполнения HTTP-вызовов и mochaделает вывод спецификаций привлекательным. Таким образом, несмотря на это, вы можете попробовать использовать этот подход и посмотреть, как он работает для вас.

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