Вариант построения NodeJs API с фреймворком Loopback. Часть 3
В предыдущей части мы завершили построение простого API на фреймворке Loopback. На текущий момент в нашем API реализованы две модели с REST методами доступа к данным, добавлены кастомные методы, сделана валидация данных. Также описаны ACL правила для выполнения действий с моделями. Но хорошо построенное API должно содержать тесты, потому что при увеличении количества кода будет расти и вероятность ошибки. Добавление тестов, конечно, не избавит от них, но позволит их выявлять до того, как они попадут продакшен.
Для добавления тестов установим два дополнительных пакета mocha и chai:
npm i mocha chai --save-dev
После успешной установки добавим в раздел scripts файла package.json дополнительные команды для запуска тестов:
"test": "NODE_ENV=test mocha tests/**/*.tets.js"
"test:watch": "npm run test -- --watch"
После этого в корне проекта создадим папку tests, куда и будем складывать наши тесты.
Добавим в папку tests файл common.js, где мы сделаем импорт файла сервера и chai. В дальнейшем мы его будем подключать во все наши файлы с тестами.
'use strict';
const app = require('../server/server');
const chai = require('chai');
const expect = chai.expect;
const request = supertest(app);
module.exports = {
app,
expect
};
В папке tests создадим папку для unit тестов наших моделей. Добавим в нее два файла: для теста функционала блогов (blog.test.js) и статей (article.test.js). В blog.test.js добавим код:
const {app, expect} = require('../common');
const Article = app.models.Article;
const Blog = app.models.Blog;
describe('Blog - It should resolve', function () {
it('A blog find', function () {
return Blog
.find()
.then(res => console.log(res));
});
});
Это описание первого теста. Мы проверяем, что у нас работает поиск блогов методом find. Что данная операция нам разрешена и не имеет никаких ограничений. Аналогичный код пишем в файле article.test.js, только заменив класс блога (Blog) на класс статьи (Article). И пришло время запустить наши тесты. Как мы видим, тесты отработали корректно, но они выводят на данные с текущей базы данных, что не очень хорошо для тестового окружения.
Для того, чтобы определить для тестового окружения свою базу данных, необходимо создать файл с источниками данных для тестов (datasource.tets.json). Просто скопируем его из datasource.json и заменим ключ file на false. И перезапустим тесты:
"file": false
Давайте теперь добавим тесты для проверки правил валидации.
Для модели блога (Blog):
describe('Blog - Validation', function () {
it('It should reject Blog title less the 3 characters', function ()
{
return Blog.create({title: 'Fi'})
.then(res => Promise.reject('Blog should not be created'))
.catch(err => {
expect(err.message).to.contain('`title` too short');
expect(err.statusCode).to.be.equal(422);
})
});
it('It should reject Blog duplicate title', function () {
return Promise.resolve()
.then(() => Blog.create({title: 'My own blog'}))
.then(() => Blog.create({title: 'My own blog'}))
.then(res => Promise.reject('Second blog record should not be created'))
.catch(err => {
expect(err.message).to.contain('`title` is not unique');
expect(err.statusCode).to.be.equal(422);
})
});
});
И для модели статьи (Article):
describe('Article - Validation', function () {
it('It should reject Article title less the 3 characters', function () {
return Article.create({title: 'Fi'})
.then(res => Promise.reject('Article should not be created'))
.catch(err => {
expect(err.message).to.contain('`title` too short');
expect(err.statusCode).to.be.equal(422);
})
});
it('It should reject Article duplicate title', function () {
const title = new Date().toDateString();
const body = new Date().toDateString();
return Promise.resolve()
.then(() => Blog.create({title: 'My own PHP blog'}))
.then((res) => {
return Article.create({title: `My own article`, body: `Article ${body}`, blog_id: res.id});
})
.then((res) => {
return Article.create({title: `My own article`, body: `Article ${body}`, blog_id: res.id});
})
.then(res => Promise.reject('Second Article record should not be created'))
.catch(err => {
expect(err.message).to.contain('`title` is not unique');
expect(err.statusCode).to.be.equal(422);
return Article.destroyAll();
})
.then(() => Blog.destroyAll())
});
it('Create an article', function () {
const title = new Date().toDateString();
const body = new Date().toDateString();
return Promise.resolve()
.then(() => Blog.create({title: 'My own PHP blog'}))
.then((res) => {
return Article.create({title: `Article ${title}`,
body: `Article ${body}`, blog_id: res.id});
})
.then(res => {
expect(res.title).to.contains(`Article ${title}`);
return Article.destroyAll();
})
.then(() => Blog.destroyAll());
});
});
Нам осталось написать тесты для проверки кастомного метода, хуков и ACL доступа к API.
Так как кастомный метод у нас в виде метода объекта и добавлен к прототипу объекта, то для его тестирования создадим объект статьи (Article) и вызовем метод vote, созданного объекта. Далее в callback функциях будем проверять, как увеличивается и уменьшается счетчик голосов за статью:
describe('Article - Custom methods', function () {
it('Vote article', function () {
const title = new Date().toDateString();
const body = new Date().toDateString();
let article = new Article({title: `Article ${title}`, body: `Article ${body}`, blog_id: 1});
article.vote(true, function (err, result) {
expect(result.votes).to.be.equal(1);
article.vote(false, function (err, result) {
expect(result.votes).to.be.equal(0);
article.vote(false, function (err, result) {
expect(result.votes).to.be.equal(0);
});
});
});
});
});
Чтобы проверить хук по запрету удаления блога со статьями, нам необходимо сначала создать новый блог, затем создать статью в этом блоге и затем попытаться удалить этот блог. В ответ, конечно же, сервер должен присылать сообщение об ошибке:
describe('Blog - Hooks', function () {
it('should not delete Blog with Articles', function () {
return Promise.resolve()
.then(() => Blog.create({title: 'My interesting blog'}))
.then(res => Article.create({title: `Article in the blog`, body: `Article 111`, blog_id: res.id}))
.then(res => Blog.destroyById(res.blog_id))
.then(res => expect(res).to.equal(null))
.catch(err => {
expect(err).to.equal('Error deleting blog with articles.');
})
});
});
Для того, чтобы проверить как ведут себя ACL правила, нам необходимо установить еще один npm пакет, немного изменить конфигурацию в common.js. Давайте установим npm пакет supertest:
npm i supertest
А в файле common.js вызовем данную функцию, где, в качестве аргумента, передадим наше приложение. Вот как изменился наш файл common.js:
'use strict';
const app = require('../server/server');
const chai = require('chai');
const supertest = require('supertest');
const expect = chai.expect;
const request = supertest(app);
module.exports = {
app,
expect,
request
};
Остается только написать сами тесты для ACL. Создадим новый файл (acl.js), куда и поместим все наши проверки. В основном, нам требуется проверять только статус ответа, который нам возвращает сервер. Запрос будем отправлять с помощью методов объекта request:
describe('Blog', function () {
it('list should be 200', function () {
return request
.get('/api/Blogs')
.expect(200);
});
it('create should be 401', function () {
return request
.post('/api/Blogs')
.send({title: 'New title'})
.expect(401);
});
it('update should be 401', function () {
return request
.patch('/api/Blogs/1')
.send({title: 'New title'})
.expect(401);
});
it('delete should be 401', function () {
return request
.delete('/api/Blogs/1')
.expect(401);
});
});
Подводя итоги, можно с уверенностью сказать, что разработка API на фреймоворке Loopback имеет ряд достоинств и недостатков.
Из достоинств можно выделить следующие:
- Большая степень автоматизации создания составных частей API.
- Достаточное количество примеров приложений на разные аспекты фреймворка.
- Поддержка фреймворка командой IBM.
В качестве недостатков стоит отметить:
- Небольшое количество документации на русском языке.
- Крутая кривую обучения.
На данном этапе наше API уже готово к работе. Но для демонстрации более широкого круга возможностей фреймворка Loopback, а также для погружения в детали его настройки нам необходимо реализовать UI интерфейс для управления нашими блогами и статьями. Но об этом в другой раз.
Скачать исходники можно тут:
git clone https://github.com/dionic-i/lb-blog.git
После загрузки переходим в папку проекта:
npm i
git checkout tests
npm run dev