В предыдущей части мы завершили построение простого 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