Как создавать 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
Запуск наших тестов на этом этапе дает следующий результат.
Это означает, что наш сервер отвечает 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. Вот ссылка на исходный код для этого урока.