Как создавать REST API с помощью TDD и Adonis JS

Создание REST API с помощью Nodejs чрезвычайно удивительно. Использование подхода разработки, основанного на тестировании, для разработки этих API значительно улучшит качество вашего кода и, что наиболее важно, улучшит вашу уверенность в API.
В этом руководстве мы узнаем, как создавать надежные API-интерфейсы REST с помощью потрясающей среды AdonisJs, следуя методам разработки, основанным на тестировании.

Для практического проекта мы создадим REST API для системы аутентификации JWT. Пользователи могут зарегистрироваться и войти в приложение для создания JWT.

Предпосылки

Чтобы продолжить, вам потребуется установить Adonis CLI на ваш локальный компьютер. Если он у вас не установлен, вы можете запустить команду npm i -g @adonisjs/cli установить его.

Во-первых, мы создадим новый проект Adonis, используя интерфейс командной строки. Выполните следующую команду:
adonis new accounts-manager --api-onlyПосле создания проекта следующим шагом будет установка некоторых зависимостей, необходимых для подключения к базе данных и для тестирования. Выполните следующую команду, чтобы установить Adonis vow, который является встроенным пакетом тестирования Adonis, и sqlite3 для нашего подключения к базе данных для запуска наших тестов.

adonis install @adonisjs/vow sqlite3

‌После установки Adonis Vow будет добавлен vowfile.js в корень проекта. В этом файле удалите комментарии из следующих строк кода:

const ace = require('@adonisjs/ace')

await ace.call('migration:run', {}, { silent: true })

await ace.call('migration:reset', {}, { silent: true })

Эти строки вне кода сообщают средству запуска тестов о необходимости выполнить миграции перед запуском тестов и сбрасывают их после завершения всех тестов.
Запуск тестов сейчас с помощью adonis test команда должна пройти.

Шаг 2 — Написание нашего первого теста

Первый тест, который мы напишем, предназначен для конечной точки регистрации. Пользователь должен иметь возможность зарегистрироваться с новой учетной записью. Создайте новый функциональный тест с помощью CLI, используя следующую команду:

adonis make:test RegisterUser

Мы добавим сюда наш первый тест, в котором описывается процесс регистрации нового пользователя.

'use strict'
const Factory =  use('Factory')
const User =  use('App/Models/User')
const  { test, trait }  =  use('Test/Suite')('Register User')

trait('Test/ApiClient')

test('registers a new user and generates a jwt',  async  ({ assert, client })  =>  {
  // generate a fake user
  const  { username, email, password }  =  await Factory.model('App/Models/User').make()
  // make api request to register a new user
  const response =  await client.post('/api/register').send({
    username,
    email,
    password
  }).end()

  // expect the status code to be 200
  response.assertStatus(200)
  // assert the email and username are in the response body
  response.assertJSONSubset({
    user: {
      email,
      username
    }
  })
  // assert the token was in request
  assert.isDefined(response.body.token)
  // assert the user was actually saved in the database
  await User.query().where({ email }).firstOrFail()
})

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

Ошибка регистрации пользователя
Наш тест, конечно, провален. Для нас важно сообщение об ошибке, и это сообщение является руководством для нашего следующего шага. Мы получили 404 что означает, что конечная точка, к которой мы пытаемся получить доступ, еще не существует. Чтобы исправить эту ошибку, давайте зарегистрируем этот маршрут в файле маршрутов приложения.


Route.group(()  =>  {
  Route.post('register',  'RegisterController.store')
}).prefix('api')

Пока мы этим занимаемся, давайте сгенерируем контроллер для этого маршрута и создадим store метод.

adonis make:controller RegisterController

Добавить store метод класса контроллера:

'use strict'

class  RegisterController  {
  async  store  ()  {}
}

module.exports  =  RegisterController

Запуск наших тестов на этом этапе дает следующий результат.

Зарегистрировать пользовательский тестовый выход 2

Это означает, что наш сервер отвечает 204 код состояния, который не является тем, для чего мы утверждали. А 204 означает, что сервер ответил без содержимого. Нам нужно исправить это, реализовав желаемую функциональность:

'use strict'

const User = use('App/Models/User')

class RegisterController {
  async store({ auth, request, response }) {
    // get the user data from the request
    const { username, email, password } = request.all()
    const user = await User.create({ username, email, password })
    // generate the jwt for the user
    const token = await auth.generate(user)
    return response.ok({ user, token })
  }
}

module.exports = RegisterController

Запуск наших тестов сейчас должен пройти.

Предотвращение вечнозеленых тестов

При написании тестов для вашего приложения всегда полезно следить за тем, чтобы ваши тесты не были вечнозелеными, что означает тесты, которые никогда не дают сбоев. Эти типы тестов могут возникать из-за того, что утверждения не выполняются, или мы делаем утверждение против неправильного. Чтобы убедиться, что ваши тесты не вечнозеленые, измените утверждение и посмотрите, не сработает ли оно. Для текущего теста я изменю следующую строку кода:

// before
response.assertStatus(200) // after
response.assertStatus(403) // before
assert.isDefined(response.body.token) // after
assert.isUndefined(response.body.token)

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

Шаг 3. Проверка на наличие дубликатов писем.

Давайте добавим тест, чтобы убедиться, что наше приложение отвечает соответствующим сообщением об ошибке, если электронная почта уже принята. Добавьте следующий тест в test/functional/register-user.spec.js файл:

test('returns an error if user already exists', async ({ assert, client }) => {
  // create a new user
  const { username, email, password } = await Factory.model('App/Models/User').create()
  const response = await client.post('/api/register').send({ username, email, password }).end()
  // assert the status code is 422
  response.assertStatus(422)
  // get the errors from the response
  const { errors } = response.body
  // assert the error for taken email was returned
  assert.equal(errors[0].message, 'The email has already been taken.')
})

Выполнение наших тестов теперь дает нам ошибку сервера 500.

Теперь это не очень полезно, потому что 500 может быть что угодно. Когда мы практикуем TDD, наш следующий шаг в большинстве случаев определяется ошибкой, которую мы получаем при запуске нашего теста, но как мы поступим, если мы не знаем, что это за ошибка? Чтобы узнать, какая ошибка исходит от нашего сервера, давайте изменим обработчик ошибок в AdonisJs, чтобы он выводил ошибку на консоль, чтобы мы могли ее увидеть. Сначала нам нужно сгенерировать обработчик ошибок, запустив:

adonis make:ehandler

Это генерирует класс с именем ExceptionHandler в app/Exceptions/Handler.js файл. handle Метод этого класса обрабатывает все ошибки, возникающие в нашем приложении.
Измените его следующим образом:

...
async handle (error, { request, response }) {
  console.log(error)
  response.status(error.status).send(error.message)
}
...

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

Запуск нашего теста теперь дает нам явную ошибку, с которой мы можем работать:

{ [Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: users.email] errno: 19, code: 'SQLITE_CONSTRAINT', status: 500 }

Наша база данных уже устанавливает адрес электронной почты как уникальный, и база данных выдает ошибку, если он уже зарегистрирован.
Мы будем использовать @adonisjs/validator package, чтобы легко проверять данные перед сохранением в нашей базе данных. Сначала установите валидатор:

adonis install @adonisjs/validator

После регистрации поставщика валидатора давайте реализуем код для проверки уникальности электронной почты. Мы изменим store метод в RegisterController:

async store ({ auth, request, response }) {
  
  const { username, email, password } = request.all()
  
  const validation = await validate({ email }, {
    email: 'unique:users'
  }, { unique: 'The email has already been taken.' })
  
  if(validation.fails()) { return response.status(422).json({ errors: validation.messages() }) }
  
  const user = await User.create({ username, email, password })
  
  const token = await auth.generate(user)
  
  return response.ok({ user, token })
}
}

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

Шаг 4. Проверка необходимого имени пользователя

Добавим тест, чтобы убедиться, что username требуется для регистрации.

test('returns an error if username is not provided', async ({ assert, client }) => {
  
  const response = await client.post('/api/register').send({ username: null, email: 'test@email.com', password: 'password' }).end()
  
  response.assertStatus(422)
  
  const { errors } = response.body
  
  assert.equal(errors[0].message, 'The username is required.')
})

Запуск этого теста теперь вызывает следующую ошибку:

{ [Error: SQLITE_CONSTRAINT: NOT NULL constraint failed: users.username] errno: 19, code: 'SQLITE_CONSTRAINT', status: 500 }

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

// create a new validator
const validation = await validate({ username, email }, { email: 'unique:users', username: 'required'
}, { required: 'The {{ field }} is required.', unique: 'The email has already been taken.'
})

Запуск наших тестов сейчас должен пройти.

Наконец, давайте добавим функциональность входа в систему. Когда пользователь предоставляет свой адрес электронной почты и пароль, для него должен быть сгенерирован JWT и отправлен в виде ответа JSON. Давайте создадим набор функциональных тестов для функции входа в систему.

adonis make:test LoginUser

Затем добавим этот тест во вновь созданный файл:

'use strict'

const Factory =  use('Factory')
const User =  use('App/Models/User')
const  { test, trait }  =  use('Test/Suite')('Register User')

trait('Test/ApiClient')

test('a JWT is generated for a logged in user',  async  ({ assert, client })  =>  {
  // generate a fake user
  const  { username, email, password }  =  await Factory.model('App/Models/User').make()
  
  // save the fake user to the database
  await User.create({
    username, email, password
  })
  
  // make api request to login the user
  const response =  await client.post('api/login').send({
    email, password
  }).end()
  // assert the status is 200
  response.assertStatus(200)
  // assert the token is in the response
  assert.isDefined(response.body.token.type)
  assert.isDefined(response.body.token.token)
})

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

HttpException: E_ROUTE_NOT_FOUND: Route not found POST /api/login

Чтобы исправить эту ошибку, давайте зарегистрируем этот маршрут и создадим связанный с ним контроллер и метод.
В файл маршрутов добавим маршрут входа.

Route.group(() => {
  Route.post('login', 'LoginController.generate')
  Route.post('register', 'RegisterController.store')
}).prefix('api')

Далее мы сгенерируем контроллер:

adonis make:controller LoginController
'use strict'

class LoginController {
  async generate() {}
}
module.exports = LoginController

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

async generate ({ auth, request, response }) {
  
  const { email, password } = request.all()
  
  const token = await auth.attempt(email, password)
  return response.ok({ token })
}

Сейчас тесты проходят успешно.

Вывод

Надеюсь, теперь вы лучше понимаете, как создавать API для отдыха в соответствии с подходом TDD. Вот ссылка на исходный код для этого урока.

Похожие записи

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *