Этот пост служит введением в тестирование RESTful API Node.js с помощью Mocha (v2.3.1). среда тестирования JavaScript.

Почему тест?

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

Возьмите образец CRUD-приложения Node/Express из репозитория:

1 2

$ git clone https://github.com/mjhea0/node-mocha-chai-tutorial.git $ git checkout tags/v1

Получив v1 приложения, вручную просмотрите его и протестируйте каждую из функций CRUD через cURL (или HTTPie или Postman):

  1. Добавить новые BLOB-объекты
  2. Просмотреть все большие двоичные объекты
  3. Просмотр одного большого двоичного объекта
  4. Обновление одного большого двоичного объекта
  5. Удалить один большой двоичный объект

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

При этом установите Mocha:

1

$ npm install -g [email protected]

Мы установили это глобально, чтобы иметь возможность запускать mocha из терминала.

Структура

Чтобы настроить базовые тесты, создайте новую папку с именем «test» в корне проекта, затем добавьте в эту папку файл с именем test-server.js. Ваша структура файлов/папок теперь должна выглядеть так:

1 2 3 4 5 6 7 8 9

├── package.json ├── server │ ├── app.js │ ├── models │ │ └── blob.js │ └── routes │ └── index.js └── test └── test-server.js

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

1 2 3 4 5 6 7

describe('Blobs', function() { it('should list ALL blobs on /blobs GET'); it('should list a SINGLE blob on /blob/<id> GET'); it('should add a SINGLE blob on /blobs POST'); it('should update a SINGLE blob on /blob/<id> PUT'); it('should delete a SINGLE blob on /blob/<id> DELETE'); });

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

Логика

Чтобы добавить необходимую логику, мы будем использовать Chai (v3.2.0), библиотеку утверждений, и chai-http (v 1.0.0) для выполнения фактических HTTP-запросов и последующего тестирования ответов.

Установите их оба сейчас:

1

$ npm install [email protected] [email protected] --save-dev

Затем обновите test-server.js следующим образом:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

var chai = require('chai'); var chaiHttp = require('chai-http'); var server = require('../server/app'); var should = chai.should(); chai.use(chaiHttp); describe('Blobs', function() { it('should list ALL blobs on /blobs GET'); it('should list a SINGLE blob on /blob/<id> GET'); it('should add a SINGLE blob on /blobs POST'); it('should update a SINGLE blob on /blob/<id> PUT'); it('should delete a SINGLE blob on /blob/<id> DELETE'); });

Здесь нам потребовались новые пакеты chai и chai-http, а также наш файл app.js, чтобы делать запросы к приложению. Мы также использовали библиотеку утверждений should, поэтому мы можем использовать утверждения в стиле BDD.

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

Теперь мы можем написать наши тесты…

Тест — ПОЛУЧИТЬ (все)

Обновите первый оператор it():

1 2 3 4 5 6 7 8

it('should list ALL blobs on /blobs GET', function(done) { chai.request(server) .get('/blobs') .end(function(err, res){ res.should.have.status(200); done(); }); });

Итак, мы передали анонимную функцию с одним аргументом done (функция) оператору it(). Этот аргумент завершает тестовый пример при вызове, например, done(). Сам тест прост: мы сделали запрос GET к конечной точке /blobs, а затем подтвердили, что ответ содержит код состояния HTTP 200.

Просто, верно?

Для проверки просто запустите mocha; и если все прошло хорошо, вы должны увидеть:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

$ mocha Blobs Connected to Database! GET /blobs 200 19.621 ms - 2 ✓ should list ALL blobs on /blobs GET (43ms) - should list a SINGLE blob on /blob/<id> GET - should add a SINGLE blob on /blobs POST - should update a SINGLE blob on /blob/<id> PUT - should delete a SINGLE blob on /blob/<id> DELETE 1 passing (72ms) 4 pending

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

1 2 3 4 5 6 7 8 9 10

it('should list ALL blobs on /blobs GET', function(done) { chai.request(server) .get('/blobs') .end(function(err, res){ res.should.have.status(200); res.should.be.json; res.body.should.be.a('array'); done(); }); });

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

Как насчет тестирования POST-запроса…

Тест — ПОСТ

Судя по коду в index.js, когда новый «большой двоичный объект» успешно добавлен, мы должны увидеть следующий ответ:

1 2 3 4 5 6 7 8

{ "SUCCESS": { "__v": 0, "name": "name", "lastName": "lastname", "_id": "some-unique-id" } }

Нужны доказательства? Проверьте это, запустив {'SUCCESS': newBlob} в консоль, а затем запустите ручную проверку, чтобы увидеть, что будет зарегистрировано.

При этом подумайте о том, как бы вы написали/структурировали свои утверждения, чтобы проверить это…

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

it('should add a SINGLE blob on /blobs POST', function(done) { chai.request(server) .post('/blobs') .send({'name': 'Java', 'lastName': 'Script'}) .end(function(err, res){ res.should.have.status(200); res.should.be.json; res.body.should.be.a('object'); res.body.should.have.property('SUCCESS'); res.body.SUCCESS.should.be.a('object'); res.body.SUCCESS.should.have.property('name'); res.body.SUCCESS.should.have.property('lastName'); res.body.SUCCESS.should.have.property('_id'); res.body.SUCCESS.name.should.equal('Java'); res.body.SUCCESS.lastName.should.equal('Script'); done(); }); });

Нужна помощь, чтобы понять, что здесь происходит? Добавьте console.log(res.body) непосредственно перед первым утверждением. Запустите тест, чтобы увидеть данные, содержащиеся в теле ответа. Написанный нами тест проверяет фактическую структуру и данные из тела ответа, разбитые по каждой отдельной паре ключ/значение.

Крючки

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

Это звучит немного сложно, но с Mocha это очень просто!

Начните с добавления файла конфигурации с именем _config.js в папку «сервер», чтобы указать другой URI базы данных для целей тестирования:

1 2 3 4 5 6 7 8

var config = {}; config.mongoURI = { development: 'mongodb://localhost/node-testing', test: 'mongodb://localhost/node-test' }; module.exports = config;

Затем обновите app.js, чтобы использовать тестовую базу данных, когда переменная среды app.settings.env оценивается как test. (По умолчанию development.)

1 2 3 4 5 6 7 8 9 10 11

// *** config file *** // var config = require('./_config'); // *** mongoose *** /// mongoose.connect(config.mongoURI[app.settings.env], function(err, res) { if(err) { console.log('Error connecting to the database. ' + err); } else { console.log('Connected to Database: ' + config.mongoURI[app.settings.env]); } });

Наконец, обновите требования и добавьте хуки в сценарий тестирования:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

process.env.NODE_ENV = 'test'; var chai = require('chai'); var chaiHttp = require('chai-http'); var mongoose = require("mongoose"); var server = require('../server/app'); var Blob = require("../server/models/blob"); var should = chai.should(); chai.use(chaiHttp); describe('Blobs', function() { Blob.collection.drop(); beforeEach(function(done){ var newBlob = new Blob({ name: 'Bat', lastName: 'man' }); newBlob.save(function(err) { done(); }); }); afterEach(function(done){ Blob.collection.drop(); done(); }); ...snip...

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

Запустите тесты еще раз, чтобы убедиться, что они все еще проходят.

Тест — ПОЛУЧИТЬ (все)

Настроив хуки, давайте рефакторим первый тест, чтобы подтвердить, что блоб из beforeEach() является частью коллекции:

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15

it('should list ALL blobs on /blobs GET', function(done) { chai.request(server) .get('/blobs') .end(function(err, res){ res.should.have.status(200); res.should.be.json; res.body.should.be.a('array'); res.body[0].should.have.property('_id'); res.body[0].should.have.property('name'); res.body[0].should.have.property('lastName'); res.body[0].name.should.equal('Bat'); res.body[0].lastName.should.equal('man'); done(); }); });

Давайте посмотрим на последние три теста…

Тест — ПОЛУЧИТЬ (одиночный)

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

it('should list a SINGLE blob on /blob/<id> GET', function(done) { var newBlob = new Blob({ name: 'Super', lastName: 'man' }); newBlob.save(function(err, data) { chai.request(server) .get('/blob/'+data.id) .end(function(err, res){ res.should.have.status(200); res.should.be.json; res.body.should.be.a('object'); res.body.should.have.property('_id'); res.body.should.have.property('name'); res.body.should.have.property('lastName'); res.body.name.should.equal('Super'); res.body.lastName.should.equal('man'); res.body._id.should.equal(data.id); done(); }); }); });

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

Тест — ПОСТАВИТЬ

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20

it('should update a SINGLE blob on /blob/<id> PUT', function(done) { chai.request(server) .get('/blobs') .end(function(err, res){ chai.request(server) .put('/blob/'+res.body[0]._id) .send({'name': 'Spider'}) .end(function(error, response){ response.should.have.status(200); response.should.be.json; response.body.should.be.a('object'); response.body.should.have.property('UPDATED'); response.body.UPDATED.should.be.a('object'); response.body.UPDATED.should.have.property('name'); response.body.UPDATED.should.have.property('_id'); response.body.UPDATED.name.should.equal('Spider'); done(); }); }); });

Здесь мы попали в конечную точку /blobs с запросом GET, чтобы получить большой двоичный объект, добавленный из ловушки beforeEach(), затем мы просто добавили _id к URL-адресу для запроса PUT и обновили имя до Spider.

Тест — УДАЛИТЬ

Ну наконец то…

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19

it('should delete a SINGLE blob on /blob/<id> DELETE', function(done) { chai.request(server) .get('/blobs') .end(function(err, res){ chai.request(server) .delete('/blob/'+res.body[0]._id) .end(function(error, response){ response.should.have.status(200); response.should.be.json; response.body.should.be.a('object'); response.body.should.have.property('REMOVED'); response.body.REMOVED.should.be.a('object'); response.body.REMOVED.should.have.property('name'); response.body.REMOVED.should.have.property('_id'); response.body.REMOVED.name.should.equal('Bat'); done(); }); }); });

Вывод

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