Рубрики
Без рубрики

Построить API отдыха с Adonisjs и TDD частью 1

Хотите узнать, как использовать Adonisjs, создавая API для отдыха с использованием подхода TDD? Это учебник, который вы хотите

Автор оригинала: EQuimper.

Я играю в последнее время с Adonisjs Nodejs MVC Framework, которая очень похожа на Laravel действительно популярная PHP Framework. Я действительно начал любить подход Adonis, больше конвенции, чем конфигурация. Я также люблю тот факт, что они говорят в заголовке.

Writing micro-services or you are a fan of TDD, it all boils down to confidence. AdonisJs simplicity will make you feel confident about your code.

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

Об этом руководстве

Так что в этом руководстве мы собираемся построить вид ведра для фильмов, чтобы посмотреть. Пользователь может создать вызов, и поставить фильмы к этому. Я не знаю, не должен потрясающий проект когда-либо, но это поможет вам увидеть, как ясно, работа в Adonis Orm с отношениями. Мы также посмотрим, насколько легко эта структура сделает нашу жизнь.

В конце этого урока мы создадим услугу, где пользователь, наконец, может ввести только имя фильма и год. Мы будем использовать Themoviedb API И найти информацию об этом фильме.

Начиная

Сначала нам нужно установить Adonis CLI

npm i -g @adonisjs/cli

Чтобы убедиться, что все работает, запустите команду в вашем терминале

adonis --help

Если вы видите список команд, что означает, что это работает

Для создания проекта мы запустим эту команду в терминале

adonis new movies_challenges --api-only

Вот это создаст новый проект Call Choine_Challenges И это будет API только котельной, так что нет UI с этим.

Следуй инструкциям

cd movies_challenges

Для работы проекта команда будет

adonis serve --dev

Но для нас нам действительно не нужно, потому что все взаимодействие будет сделано из тестирования.

Откройте проект в вашем текстовом редакторе выбора. Для себя я использую VSCode Это бесплатно и потрясающе.

Настройка БД

Адонис настроил много вещей для нас. Но они дают нам выбрать некоторые вещи, такие как какие БД использовать и т. Д. Если вы открываете файл config/database.js Вы увидите SQLite , MySQL и PostgreSQL конфигурация Для этого проекта я буду использовать posgresql

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

npm i --save pg

После этого зайди свой .env Файл и настройте соединение для вашей БД. Для меня это будет выглядеть

DB_CONNECTION=pg
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_DATABASE=movies_challenges_dev

После того, как я убедиться, что создаю БД от моего терминала

createdb movies_challenges_dev

Настройте среду тестирования

Adonis не приезжает с рамки тестирования вне коробки, но это действительно легко заставить его работать.

Запустить команду

adonis install @adonisjs/vow

Что это такое ? У Adonis есть способ установить зависимость, используя NPM внутренне. Но красота этого она также может добавить другие вещи. Как если вы посмотрите, что происходит после этого, они откроют URL в вашем браузере с другими инструкциями.

Они создают 3 новых файла.

.env.testing
vowfile.js
example.spec.js

Сначала мы настроим .env.testing файл, чтобы убедиться, что мы тестируем БД, а не dev одно.

Добавьте это в конец файла

DB_CONNECTION=pg
DB_HOST=127.0.0.1
DB_PORT=5432
DB_USER=postgres
DB_PASSWORD=postgres
DB_DATABASE=movies_challenges_test

После того, как я убедиться, что создаю БД от моего терминала

createdb movies_challenges_test

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

Таким образом, приложение будет работать, – это пользователь может иметь много проблем. Эти проблемы могут иметь много фильмов к нему. Но фильм может быть для многих вызов.

Так что в отношениях это будет выглядеть

Если у вас есть немного проверяют структуру папки, вы увидите, что Adonis дает использовать пользовательскую модель и аутент коробки.

Мы будем использовать это в будущем.

Поэтому для того, чтобы сделать ваш первый тестовый файл, нам нужно будет думать о том, что нам нужно сделать.

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

Адонис даст нам много инструментов, чтобы наш жить легче. Один из них – команда генератора, спасибо Тузе. Мы будем использовать команду, чтобы сделать наш первый тест. Но, чтобы иметь возможность сделать это, нам нужно зарегистрировать структуру тестирования Vow к провайдеру проекта. Открыть Start/app.js и добавьте это на ваш AceProvider

const aceProviders = [
  '@adonisjs/lucid/providers/MigrationsProvider',
+ '@adonisjs/vow/providers/VowProvider'
]

Теперь мы можем запустить команду

adonis make:test CreateChallenge

Когда вы получаете функционал «Задачить единицу» или функциональное использование функционального теста и нажмите «Войти».

Это создаст файл Тест/функционал/create-challenge.spec.js Отказ

Хороший первый тестовый файл создавать

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

test('can create a challenge if valid data', async ({ assert }) => {})

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

test('can create a challenge if valid data', async ({ assert }) => {

  const response = // do api call

  response.assertStatus(201)
  response.assertJSONSubset({
    title: 'Top 5 2018 Movies to watch',
    description: 'A list of 5 movies from 2018 to absolutely watched',
    user_id: // to do
  })
})

Вот я проверю, чем я хочу получить от моего API Call a 201 создан Благодаря определенному объекту, у которого будет указанный заголовок, описание, которое я предоставляет, и мой текущий идентификатор пользователя.

Далее нам нужно написать код для ответа

const { test, trait } = use('Test/Suite')('Create Challenge')

trait('Test/ApiClient')

test('can create a challenge if valid data', async ({ assert, client }) => {

  const data = {
    title: 'Top 5 2018 Movies to watch',
    description: 'A list of 5 movies from 2018 to absolutely watched'
  }

  const response = await client.post('/api/challenges').send(data).end()

  response.assertStatus(201)
  response.assertJSONSubset({
    title: data.title,
    description: data.description,
    user_id: // to do
  })
})

Чтобы сделать вызов API, нам нужно импортировать первый Черта из тестового набора. Нам нужно сообщить тесту, мы хотим клиент API. Это даст нам сейчас доступ к клиент в обратном вызове. Затем я поставил свои данные, которые я хочу на объект и отправить его на маршрут с глаголом Пост Отказ

Теперь я хочу проверить с текущим пользователем JWT в заголовках. Как мы можем это сделать ? Это так просто с Adonis

'use strict'

const Factory = use('Factory')
const { test, trait } = use('Test/Suite')('Create Challenge')

trait('Test/ApiClient')
trait('Auth/Client')

test('can create a challenge if valid data', async ({ assert, client }) => {
  const user = await Factory.model('App/Models/User').create()

  const data = {
    title: 'Top 5 2018 Movies to watch',
    description: 'A list of 5 movies from 2018 to absolutely watched',
  }

  const response = await client
    .post('/api/challenges')
    .loginVia(user, 'jwt')
    .send(data)
    .end()

  response.assertStatus(201)
  response.assertJSONSubset({
    title: data.title,
    description: data.description,
    user_id: user.id,
  })
})

МОЙ БОГ !!! Слишком. Не волнуйся. Нам просто нужно немного сломать его. Так что сначала что такое фабрика. Фабрика – это способ сделать манекенные данные проще. Это приходит с действительно хорошей API. Здесь завод создаст пользователя в БД. Но как завод может знать данные, которые мы хотим? Легко просто откройте База данных/Factory.js файл и добавьте это внизу

const Factory = use('Factory')

Factory.blueprint('App/Models/User', faker => {
  return {
    username: faker.username(),
    email: faker.email(),
    password: 'password123',
  }
})

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

Так что эта линия

const user = await Factory.model('App/Models/User').create()

Мы создаем пользователя DB, теперь мы можем использовать этот же пользователь здесь, в запросе

const response = await client
  .post('/api/challenges')
  .loginVia(user, 'jwt')
  .send(data)
  .end()

Как видите, мы теперь можем использовать loginvia и пропустить пользователю при первом аргументе, второй аргумент – это тип аутеринга здесь, я говорю JWT. Я могу использовать .Loginvia Причина этой четки на вершине

trait('Auth/Client')

Теперь в моем ответе JSON Теперь я могу проверить идентификатор пользователя действительно одним из текущих пользователей

response.assertJSONSubset({
  title: data.title,
  description: data.description,
  user_id: user.id,
})

Мы думаем, что нам нужно сделать, прежде чем идти дальше, и запустите тест, мы должны увидеть ошибку из ответа, чтобы сделать реальную TDD.

Итак, мы добавим эту строку до утверждения

console.log('error', response.error)

Теперь мы можем запустить тест с помощью команды Тест Адониса

Вы увидите ошибку

error: relation "users" does not exist

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

На линии 14 const ('@ adonisjs/Ace') На линии 37 aquait Ace.call («Миграция: run», {}, {Silent: true}) На линии 60 await ace.call («Миграция: сброс», {}, {silent: true})

Теперь, если вы перезагрузите тест, вы увидите

error { Error: cannot POST /api/challenges (404)

Хороший шаг дальше Эта ошибка означает, что у нас нет маршрута. Нам нужно создать это. Открыть Запуск/маршруты .JS и добавить этот код

Route.post('/api/challenges', 'ChallengeController.store')

Здесь я говорю, когда мы получаем почтовый запрос на маршрут /API/ВЫЗОВЫ Передайте данные в ChallengeController контроллера и хранилище методов. Помните, что Adonis – MVC, поэтому да, нам нужен контроллер

Сохраните код и перезагрузите тест

Теперь в тексте ошибки вы увидите

Error: Cannot find module \'/Users/equimper/coding/tutorial/movies_challenges/app/Controllers/Http/ChallengeController\'

Это означает, что контроллер не существует, поэтому нам нужно создать один. Опять Адонис у генератора для этого

adonis make:controller ChallengeController

Когда попросите выбрать HTTP не Websocket

Повторный тест

'RuntimeException: E_UNDEFINED_METHOD: Method store missing on App/Controllers/Http/ChallengeController\n> More details: https://err.sh/adonisjs/errors/E_UNDEFINED_METHOD'

Способ хранения отсутствует. Хорошо, что это нормально, контроллер пусто. Добавьте это в свой файл Приложение/Контроллеры/http/challengeController.js

class ChallengeController {
  store() {}
}

Повторный тест

expected 204 to equal 201
204 => 201

Итак, теперь именно здесь веселое начало, мы ожидали 201, но получили 204. Мы можем исправить эту ошибку, добавив

class ChallengeController {
  store({ response }) {
    return response.created({})
  }
}

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

 expected {} to contain subset { Object (title, description, ...) }
  {
  + title: "Top 5 2018 Movies to watch"
  + description: "A list of 5 movies from 2018 to absolutely watched"
  + user_id: 1
  }

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

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

class ChallengeController {
  async store({ response, request }) {
    const challenge = await Challenge.create(
      request.only(['title', 'description'])
    )

    return response.created(challenge)
  }
}

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

Теперь, если я повторю тест, я вижу

'Error: Cannot find module \'/Users/equimper/coding/tutorial/movies_challenges/app/Models/Challenge\''

Хорошо иметь смысл, что модель не существует

adonis make:model Challenge -m

-M дают вам файл миграции также

Эта команда будет создана

✔ create  app/Models/Challenge.js
✔ create  database/migrations/1546449691298_challenge_schema.js

Теперь, если мы вернем тест

'error: insert into "challenges" ("created_at", "description", "title", "updated_at") values ($1, $2, $3, $4) returning "id" - column "description" of relation "challenges" does not exist'

Имейте смысл, что таблица не имеет описания столбца. Поэтому мы должны добавить один

Так что откройте свой файл миграции для Challenge_schema

class ChallengeSchema extends Schema {
  up() {
    this.create('challenges', table => {
      table.text('description')
      table.increments()
      table.timestamps()
    })
  }

  down() {
    this.drop('challenges')
  }
}

Здесь я добавляю колонку текст Описание звонка

Повторный тест

'error: insert into "challenges" ("created_at", "description", "title", "updated_at") values ($1, $2, $3, $4) returning "id" - column "title" of relation "challenges" does not exist'

Теперь та же ошибка, но для заголовка

class ChallengeSchema extends Schema {
  up() {
    this.create('challenges', table => {
      table.string('title')
      table.text('description')
      table.increments()
      table.timestamps()
    })
  }

  down() {
    this.drop('challenges')
  }
}

Здесь название будет строка. Теперь повторный тест

  expected { Object (title, description, ...) } to contain subset { Object (title, description, ...) }
  {
  - created_at: "2019-01-02 12:28:37"
  - id: 1
  - updated_at: "2019-01-02 12:28:37"
  + user_id: 1
  }

Очера означает заголовок и описание, а user_id не существует, поэтому нам нужно добавить отношение в миграцию и модель

Снова в миграционном файле добавить

class ChallengeSchema extends Schema {
  up() {
    this.create('challenges', table => {
      table.string('title')
      table.text('description')
      table
        .integer('user_id')
        .unsigned()
        .references('id')
        .inTable('users')
      table.increments()
      table.timestamps()
    })
  }

  down() {
    this.drop('challenges')
  }
}

Здесь user_id – это целое число, ссылаться на идентификатор пользователя в таблице пользователей

Теперь откройте модель вызова в Приложение/Модели/Challenge.js и добавить этот код

class Challenge extends Model {
  user() {
    this.belongsTo('App/Models/User')
  }
}

И нам нужно сделать другой способ отношения, так что открыть Приложение/Модели/user.js и добавить внизу после токенов

challenges() {
  return this.hasMany('App/Models/Challenge')
}

Вау, я люблю этот синтаксис и насколько легко мы видим отношения. Спасибо команды Adonis и Lucid Orm

Запустить тест

 expected { Object (title, description, ...) } to contain subset { Object (title, description, ...) }
  {
  - created_at: "2019-01-02 12:35:20"
  - id: 1
  - updated_at: "2019-01-02 12:35:20"
  + user_id: 1
  }

Та же ошибка? Да, когда мы создаем, мы не поместили user_id. Так что нам нужно

class ChallengeController {
  async store({ response, request, auth }) {
    const user = await auth.getUser()

    const challenge = await Challenge.create({
      ...request.only(['title', 'description']),
      user_id: user.id,
    })

    return response.created(challenge)
  }
}

Здесь я использую Auth, который является объектом, который мы способ, касающийся аутентификации. Здесь я могу использовать текущий пользователь с функцией auth.getuser. Это вернет пользователю от JWT. Теперь я могу объединить это на объект при создании.

Теперь, если вы запустите свой тест, все должны работать. Buttttt это не сделано. Нам нужен тест, чтобы убедиться, что пользователь действительно аутентифицируется, потому что теперь эта конечная точка доступна всем.

Добавить в наш тестовый файл

test('cannot create a challenge if not authenticated', async ({
  assert,
  client,
}) => {})

Опять же, мы собираемся работать с той же идеей, построение утверждения сначала и возвращаясь назад

test('cannot create a challenge if not authenticated', async ({
  assert,
  client,
}) => {
  response.assertStatus(401)
})

Здесь мы хотим, чтобы статус был 401 несанкционирован

test('cannot create a challenge if not authenticated', async ({
  assert,
  client,
}) => {
  const data = {
    title: 'Top 5 2018 Movies to watch',
    description: 'A list of 5 movies from 2018 to absolutely watched',
  }

  const response = await client
    .post('/api/challenges')
    .send(data)
    .end()

  console.log('error', response.error)

  response.assertStatus(401)
})

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

Откройте файл своих маршрутов

Route.post('/api/challenges', 'ChallengeController.store').middleware(['auth'])

Если вы запустите тест, все будет зеленым

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

Adonis дают нам доступ к другому действительно приятному инструменту.

Нам нужно установить библиотеку валидатора

adonis install @adonisjs/validator

Перейти к Start/app.js и добавить провайдер

const providers = [
  '@adonisjs/framework/providers/AppProvider',
  '@adonisjs/auth/providers/AuthProvider',
  '@adonisjs/bodyparser/providers/BodyParserProvider',
  '@adonisjs/cors/providers/CorsProvider',
  '@adonisjs/lucid/providers/LucidProvider',
+  '@adonisjs/validator/providers/ValidatorProvider'
]

Теперь вернитесь в наш тестовый файл для вызова и добавьте новый

test('cannot create a challenge if no title', async ({ assert }) => {})

Прежде чем идти дальше, мне не нравится, что мне нужно вручную написать название и описание. Я хотел бы иметь возможность заставить завод создать это для нас. Это возможно, сначала переходите к База данных/Factory.js.

Нам нужно создать фабрику для вызова

Factory.blueprint('App/Models/Challenge', faker => {
  return {
    title: faker.sentence(),
    description: faker.sentence()
  }
}

Теперь мы можем использовать это с помощью Make

const { title, description } = await Factory.model(
  'App/Models/Challenge'
).make()

Это даст нам поддельное название и описание, но, не будучи сэкономить до БД.

Возвращаясь к тесту будет получать ошибку, если заголовок не находится в теле

test('cannot create a challenge if no title', async ({ assert, client }) => {
  response.assertStatus(400)
  response.assertJSONSubset([
    {
      message: 'title is required',
      field: 'title',
      validation: 'required',
    },
  ])
})

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

test('cannot create a challenge if no title', async ({ assert, client }) => {
  const user = await Factory.model('App/Models/User').create()
  const { description } = await Factory.model('App/Models/Challenge').make()

  const data = {
    description,
  }

  const response = await client
    .post('/api/challenges')
    .loginVia(user, 'jwt')
    .send(data)
    .end()

  response.assertStatus(400)
  response.assertJSONSubset([
    {
      message: 'title is required',
      field: 'title',
      validation: 'required',
    },
  ])
})

Сначала мы создаем пользователь, чтобы иметь возможность войти в систему, потому что нам нужно аутентифицировать, запомнить

Во-вторых, я получаю поддельное описание с моей фабрики. Я просто отправляю это.

Я утверждаю, что я получу 400 для плохого запроса и массив JSON сообщения об ошибке.

Если я пробежу тест, теперь я получаю

expected 201 to equal 400
  201 => 400

Это означает, что проблема создается, но не должна

Поэтому нам нужно добавить валидатор для этого

adonis make:validator CreateChallenge

Идите в файл ваших маршрутов, и мы хотим использовать это

Route.post('/api/challenges', 'ChallengeController.store')
  .validator('CreateChallenge')
  .middleware(['auth'])

Теперь, если вы запустите тест, вы увидите

expected 201 to equal 400
  201 => 400

Почувствуйте смысл валидатора разбить вещи. Время написать какой-то код. Открыть Приложение/валидаторы/CreateChallenge.js

class CreateChallenge {
  get rules() {
    return {
      title: 'required|string',
      description: 'string',
    }
  }

  get messages() {
    return {
      required: '{{ field }} is required',
      string: '{{ field }} is not a valid string',
    }
  }

  get validateAll() {
    return true
  }

  async fails(errorMessages) {
    return this.ctx.response.status(400).json(errorMessages)
  }
}

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

Если вы запустите тест, теперь все должны работать

Мы также можем добавить неTULLUBLE поле в столбец заголовка в миграциях

table.string('title').notNullable()

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

test('cannot create a challenge if title and description are not a string', async ({
  assert,
  client
}) => {
  const user = await Factory.model('App/Models/User').create()

  const data = {
    title: 123,
    description: 123
  }

  const response = await client
    .post('/api/challenges')
    .loginVia(user, 'jwt')
    .send(data)
    .end()

  response.assertStatus(400)
  response.assertJSONSubset([
    {
      message: 'title is not a valid string',
      field: 'title',
      validation: 'string'
    },
    {
      message: 'description is not a valid string',
      field: 'description',
      validation: 'string'
    }
  ])
})

И если мы снова запустим тестовый бум все зеленые.

Конец слово

Это из моего блога здесь

Код можно найти здесь на гадость