Я играю в последнее время с 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' } ]) })
И если мы снова запустим тестовый бум все зеленые.
Конец слово
Это из моего блога здесь
Код можно найти здесь на гадость