Написание контроллеров регистрации и их функциональное тестирование. Добавление локальных фикстур для тестовых сценариев. Проверка отправки электронных писем.
- 00:00:32 Постановка задачи
- 00:01:32 Доработка NotFoundTest
- 00:02:45 HTTP-клиент в PhpStorm
- 00:04:00 Тестирование части JSON
- 00:06:40 Вынос декодирования JSON
- 00:07:16 Отправка POST-запросов с телом
- 00:08:06 Куда поместить экшены в модульной архитектуре
- 00:16:47 Адресация и версионирование API
- 00:19:13 Настройка интервала для токенов
- 00:20:09 Тест запроса регистрации
- 00:22:52 Создание контроллера регистрации
- 00:23:54 Пустой ответ с EmptyResponse
- 00:27:02 Очистка записей предыдущих тестов
- 00:28:25 Неудобство общих фикстур
- 00:30:27 Локальные фикстуры для теста
- 00:32:52 Метод загрузки фикстур для теста
- 00:35:28 Оптимизация подключения к БД в фикстурах
- 00:39:00 Восстановление демо-данных после тестов
- 00:44:02 Тестирование отправки писем в MailHog
Скрытый контент (код, слайды, ...) для подписчиков.
Открыть →Чтобы не пропускать новые эпизоды подпишитесь на наш канал @deworkerpro в Telegram
Спасибо за урок, Дмитрий!
У меня есть вопросы:
1) Не удобнее было бы в тестах использовать отдельную базу данных для тестового окружения? Тогда после тестов не пришлось бы опять накатывать фикстуры.
2) При использовании контроллера вне изолированных контекстов бывают сценарии, когда есть необходимость вызвать несколько интеракторов из разных контекстов (по событию или же через "оркестратор"). Есть ли один из них при этом успешно модифицировал данные, а следующий выбросил исключение, следует откатить действие первого. Планируется ли рассмотреть подобный сценарий?
3) Планируется ли рассмотреть Graphql для озвученной в видео возможности чтения запрашиваемых клиентом дополнительных данных у эндпойнта?
Да, есть альтернативы работы с той же БД:
1) Вызывать
$em->getConnection()->beginTransaction()
в методеsetUp()
и вызыватьrollback()
вtearDown()
. Тогда тест будет выполняться в транзакции и после теста все данные будут восстанавливаться назад.2) Подключить ещё один сервис
api-postgres-test
и указать его в тестовом окружении. И дополнительно накатывать миграции ещё и на неё.Эти способы сработают только для кода внутри нашего внутреннего теста.
Но это не сработает когда мы будем внешними приёмочными тестами проверять фронтенд, дёргающий наш API с оригинальной БД. Так что приёмочные тесты для всей системы будут ходить в оригинальные БД. В итоге загрузку фикстур всё равно придётся производить после каждого запуска приёмочных.
Также когда добавятся очереди у нас в фоне будут запущены консьюмеры, которые тоже будут ходить в оригинальную БД. В этом случае функциональный тест в тестовую БД или внутри транзакции в оригинальную запишет фикстуру и отправит сообщение в очередь, а воркер эту запись в его оригинальной БД не увидит. В итоге в функциональные тесты придётся изолировать заглушками, чтобы они не отправляли ничего в очередь. Это будет быстрее, но такой тест с заглушкой очереди теперь не будет проверять работоспособность реальной очереди.
Так что как удобнее совместить разработку и тестирование - вопрос весьма сложный.
Я придумал еще третий вариант)
Можно перед запуском тестов делать дамп базы, а по окончании тестов восстанавливать его. Я вижу 2 преимущества в этом подходе:
Как вам такой способ?)
Да, это паттерн "Сага". Его рассмотрим.
Расскажем, что универсальные решения удобны для CRUD-подхода, когда API является отражением один-в-один таблиц в БД. Но если делаем что-то кастомное, то проявляются неудобства.
Так что вдальнейшем сравним подходы к написанию API и выберем оптимальный вариант.
Спасибо за ответы!
пересмотрел -> большое спасибо.
Попробую сформулировать свой вопрос.
Допустим, что у нас появилась необходимость из модуля блог дернуть авторизацию. То как то правильно сделать, чтобы не напрямую дергать код из авторизации? Чтобы модули не были напрямую зависимы друг от друга.
Надо как то дергать контроллер? Надо вводить какую - то абстракцию для этого?
Когда ту же авторизацию вынесут на отдельный сервер из -за нагрузки, допустим, то код просто исчезнет из нашей кодовой базы и надо будет как - то переключить вызовы на другую апи. Как это все реализовать правильно, чтобы не было потом больно? Как абстрагировать свои внутренние вызовы, чтобы потом можно было просто все поправить в одном месте?
Обычно авторизация производится на уровне контроллеров. При нашем подходе с отдельными контроллерами проблем с не будет. А роль пользователя для привязки прав будем передавать напрямую в токене аутентификации.
Спасибо за уроки! Появилось два вопроса:
1) Если представить такую ситуацию. Нам необходимо вынести модуль Auth на отдельный сервер, когда производится аутентификация через OAuth2, токены для пользователя записываются в БД, которая отдельная/своя для модуля Auth. А как пользователь с этим токеном сможет достучаться до модуля Auction, если у него другая БД? Нам нужно при получении пользователем токена в микросервисе Auth отправлять его еще и в микросервис Auction и остальные микросервисы, лежащие на разных серверах с разными БД например через очередь? Или для такого используют другой подход?
2) Должен ли модуль Auth знать о доменных ролях? На например пользователь регистрируется на сайте и может иметь роль автора блога, либо читателя блогов и при регистрации он должен указать в качестве кого он хочет зарегистрироваться. Или модуль Auth будет иметь только роли ROLE_ADMIN, ROLE_USER. А роли ROLE_AUTHOR, ROLE_READER будут уже внутри модуля Blog, и будут выступать в виде сущностей Author и Reader?
Для этого используются не простые токены, а JWT, которые представляют собой зашифрованный JSON с идентификатором и ролью пользователя. Тогда Auction напрямую берёт
user id
прямо из токена и никуда больше не ходит.Второй вариант, когда каждый модуль хранит свои сущности вроде Author и Reader. В этом случае доменные AUTHOR, READER можно делать не ролями, а флагами вроде
is_author
или наличием/отсутствием этих Author и Reader.А что касается общих ролей и разрешений проекта, то они обычно проверяются в контроллерах, а не в модулях. Например, общая роль MODERATOR может предполагать доступ к модерации лотов и комментариев, поэтому её ни в какой модуль поместить не получится.
Поэтому общие роли можно хранить в Auth, а разрешения уже привязывать и проверять на уровне Http.
Спасибо за ответы, Дмитрий!
Добрый день, Дмитрий!
Подскажите, пожалуйста, как лучше реализовать собственные обработчики ошибок 404 и 405 взамен тех, что существуют в Slim по умолчанию и возвращают ответ в формате HTML. В частности, необходимо на любые запросы к API (правильные или ошибочные) всегда возвращать свой ответ в формате JSON.
В итоге добавил вызов forceContentType('application/json') и написал свой JsonErrorRenderer, выводящий ответ в нужном виде.
Достаточно переопределить обработчик по умолчанию с
html
наjson
:Не могу понять, мне выдает что
routes:
Handler.php
Не понятно, что в нём не может зарезолвится, помогите пожалуйста
Вы объявили конструктор приватным.
Да, я заметил потом, прошу прощения, забыл отписаться. Благодарю!
Здравствуйте Дмитрий! Спасибо за вашу работу, все реально доходчиво и полезно!
Столкнулся с такой проблемой при написании RequestAction:
Тесты все проходят - создается пользователь, отправляется письмо, если не очищать базу то повторно тест не проходит возвращается ошибка что пользователь уже есть - в общем все работает так как в тестах предусмотрено. При отправке запроса:
с невалидными данными возвращаются ошибки валидации, но если отправить этот запрос с правильными (валидными) данными ожидается что если базу не чистить вернется ошибка Пользователь есть или будет создан Пользователь по запросу и вернется пустой JSON - но ответ не ожиданный:
Это получается что у меня при вызове контроллера RequestAction не видно Doctrine? Какая то ошибка в путях? Но тогда и тесты по идее не должны проходить! Буду очень признателен если поможете с решением этой проблемы! Спасибо!
Проблема была в отсутствии
в Docker файле php-fpm. После добавления все стало работать ожидаемо. Но вопрос о прохождении тестов остался - можно ли как то при тестировании такую ситуацию смоделировать? Тесты проходили все время - а проблема обнаружилась случайно!
Функциональные тесты в консоли запускаются в php-cli, а не в php-fpm. А тестирование работы в nginx+php-fpm у нас будет производиться именно в E2E-тестах, когда из фронтенда будут посылаться реальные запросы к API.
Теоретически (ну и практически судя по всему) эта ошибка всплыла бы немного позже! Спасибо! Еще раз вам огромное спасибо за ваш труд!
Ребята, кто дошел до этой темы разрабатывая на Винде, пожалуста поделитесь, как вы собрали своё окружение разработки (vagrant, virualbox PhpStorm). Дошел с бубном до 10го урока и сдался, 10 уроков не разработкой занимался , а ДевОпс-ом у себя работал. На линукс пытался переехать, но и там не всё просто :)
Почему делаем Фикстуру для каждего реквеста, а не просто создадим один класс UserFixture с кучей методов "load_что_нужно" ?
Метод
load
в фикстуре предусмотрен всего один.А не помещаем всех пользователей в один класс
UserFixture
потому, что со временем один общий файл становится именно огромной кучей непонятно чего. С маленькими простыми классами работать удобнее, чем с одним огромным.И помимо этого, в тестах пользователи для одного теста могут мешать другим тестам. Например, если тест проверяет вывод числа пользователей, то добавление любого другого пользователя в общую фикстуру будет каждый раз сбивать все счётчики. Поэтому удобнее для каждого теста делать свою отдельную фикстуру, чтобы этому тесту никто не мешал.
Здравствуйте, Дмитрий! Хорошие впечатления получил от вашей серии уроков -- особенно по девопсу и тестам -- таких как вы по пальцам пересчитать )
Хотел спросить про тестирование контроллеров. На первый взгляд все просто -- формируй клиента, стучись в эндпоинт, ожидай, что произвелись нужные изменения и/или вернулось корректное состояние.
Но вот незадача, если в этом эндпоинте происходит работа с брокером сообщений через тот же Messenger в Symfony с использованием RabbitMQ.
Вот как здесь производить тестирование ? Есть ли какие-нибудь источники, на которые можно было бы ориентироваться при написании таких тестов ?
Я сам нашел вот этот источник: https://codeburst.io/unit-functional-test-the-symfony4-messenger-9eef328dce8 Правда здесь скорее описывается интеграционный тест, нежели функциональный
А хотелось бы также написать функциональные тесты для такого рода эндпоинтов, в которых используется брокер сообщений. Буду очень признателен за любую информацию!)
Можно подменять клиент очереди моками, как вы сказали превращая тест в интеграционный. Но при этом есть риск, что будут ошибки в реальной очереди.
Но вместо этого можно тестировать с реальной очередью, ожидая результат.
Например, если отправку письма подтверждения регистрации мы перенесём в очередь, то вместо простой проверки:
можно сделать проверку с ожиданием:
И там уже в цикле вызывать эту функцию, пока она не вернёт непустой результат:
Либо это оформить в виде кастомного assert-а для PHPUnit.
Любопытная идея
Благодарю, Дмитрий!)
Вот на этом месте я сломался )) Если раньше почти все получалось, то сейчас упорно тесты не проходят, и понять почему я не смог...
Если кто по моим граблям пойдет - в routes.php маршруты вписать не забудьте )
Точней почему не проходят - понятно. Не понятно почему всегда 404 прилетает...
А если нам нужно добавить в регистрацию дополнительное поле, например ФИО. Оно вроде как никакого отношения к аутентификации не имеет. Т.е. мы создаем модуль "Учетная запись", для работы с профилем. И в экшене джоина обращаемся еще и к ней?
Да, профиль сохраняем в отдельный модуль.
А на фронтенде и в экшене даже можно это не совмещать. Можно сделать духшаговый процесс, где сначала выводим форму регистрации, а уже потом после регистрации просим ввести имя и прочие данные. Это кому как удобнее.
А почему ФИО + дата рождения не может быть в системе аутентификации? По этим двум данным можно идентифицировать человека. Или я не прав? Во всех системах сейчас стараются использовать ФИО, дата рождения, телефон. Например в Яндекс ID или VK ID. То есть это часть этого модуля. Другое дело, когда есть какой-то сервис блога и там свои авторы. Вот это уже действительно профиль автора.
Если говорить именно про программную аутентификацию, то ей из всего человека интересны только данные для входа (email с паролем, телефон для SMS-кода или id из внешней OAuth системы), по которым можно идентифицировать (определить) id пользователя.
Если мы программируем всё модульно и микросервисно, то стараемся декомпозировать данные так, чтобы в каждый модуль помещать только минимально необходимую для работы этого модуля или микросервиса информацию. Ничем лишним их не захламляем.
Другие поля вроде ФИО и пола процессу аутентификации неинтересны. Если дата рождения и пол нужны только для бонусной программы чтобы давать скидки в день рождения и 8 марта, то мы их поместим в модуль бонусной программы, а не в модуль аутентификации.
В VK ID есть данные входа (почта, пароль, телефон), ФИО, пол, дата рождения, почта для уведомлений и история платежей.
В Яндекс ID есть данные входа (почта, пароль, телефон, биометрия), ФИО, пол, дата рождения, банковские карты, документы (паспорт, СНИЛС, страховой полис), адреса, члены семьи, автомобили, питомцы, публичный профиль.
Это у них большой сайт-фронтенд размером почти с Госуслуги. То есть это не просто модуль аутентификации, а вынесенный на отдельный поддомен личный кабинет, где выводится и вносится вся сводная информация.
А в коде бэкенда это уже вполне вероятно разбито на модули или микросервисы, где данные входа (почта, пароль, телефон, биометрия) как раз могут лежать в микросервисе аутентификации, автомобили в микросервисе страховки, карты в микросервисе оплаты и т.д.
Спасибо, доступно объяснили.
Единственное не очень понял где именно хранится ФИО и Дата рождения у подобных сервисах VK ID и Яндекс ID. Ведь их нужно ввести при регистрации, а после аутентификаци Яндекс и ВК добавляет в свой JWT токен эту информацию. Обычно ещё добавляет Аватарку в этот же JWT. Вот пример ответа: https://yandex.ru/dev/id/doc/ru/tokens/jwt#jwt-response
Если данные хранить в разных модулях, то это будет проблема при выдаче такого токена. Так как придется данные собирать с разных модулей. А с учетом того, что модуль может быть вынесен в микросервис, то может быть проблема с доступностью этих сервисов. Как в этом случае это всё работает? Я думал, что эти данные всё таки идут в сервис аутентификации. Кроме того он работает с персональными данными…
Ну это Яндекс так заморочился, что помимо получения информации о пользователе в формате JSON:
сделал возможным получать отсюда же то же самое в формате JWT:
Это у него вернёт токен со стандартным полем uid и с этими дополнительными полями.
Может это и даёт Яндексу и VK плюс в плане производительности, что фронтендеры могут брать имя и фото напрямую из JWT, не запрашивая с сервера
/info
снова и снова.Других видимых причин добавлять всё в токен вместо отдельного вызова
/info
особо нет.Бэкендерам ориентироваться на эти данные из JWT будет не очень надёжно, так как на сервере телефон может уже поменяться, а в токене останется вписан старый.
Как сервер получает данные в контроллере
/info
для вывода какой-либо сводной информации, так он получает эту же информацию для вписывания JWT. Здесь в плане надёжности никакой разницы нет.А как это устроено внутри Яндекса и как это всё у него лежит внутри мы не узнаем.
Понял, спасибо)
Ну и, кстати, ещё для путаницы по фронтендам.
OAuth-вход для интеграций работает по адресу:
Потом по API информация о пользователе достаётся по:
При входе на сайт из браузера открываются:
Данные и пароль выводятся и меняются в:
Так что большой вопрос, где и как там это всё там лежит на сервере.
Да, я тоже пытался понять именно по Яндексу где, что и как хранится и не нашел ответа. Для себя сделал вывод, что это у них это один сервис. Вывод такой был потому что JWT выдает эти данные сразу. Сервис должен быть надёжным, а значит вряд ли идут запросы по сети к Profile при генерации токена. Ну или как-то сильно извернулись.
Скорее, всего, как это бывает в больших проектах, это Легаси. Я отлично помню, что Яндекс ID раньше назывался Яндекс Паспорт. Соотвественно, старые URL не убрали из процессов. Частично переписывают.
Спасибо за обратную связь. Интересно было ваше мнение)
У себя в проектах я решил, что если один раз себе дать слабину и поместить в аутентификацию ФИО, то потом по аналогии захочется туда же впихнуть дату рождения, аватарку, адрес и всё остальное.
А потом захочется конвертировать аватарки в разные форматы и туда придётся вписывать код конвертера изображений. Потом захочется сделать автоподстановку адреса и туда придётся впрограммировать код хождения в API в сторонние сервисы адресов.
В итоге такой модуль у меня сильно располнеет и обрастёт связями из других мест на эти поля. А если сделаю модуль какого-нибудь профиля или несколько их отдельно от модуля аутентификации, то захламления не будет. И при создании нового проекта смогу спокойно скопировать такой модуль аутентификации туда если там ФИО и другие поля не будут нужны.
Прекрасно Вас понимаю, потому что уже приходилось разделять это. У аутентификации и так много всякой логики, так что в этом я согласен. Я тоже пришел к тому что профиль это отдельное. Кроме того профилей может быть несколько у одного пользователя, например, детский профиль. Вход осуществляется через родителя, а данные свои.
Есть такой вопрос. На каком уровне лежит ответственность проверки доступа к ресурсу? Например, может ли автор редактировать конкретную статью?
Обычно в существующих фреймворках вроде Symfony, Laravel и Yii такие проверки делают через их систему авторизации в контроллере по RBAC или как-то ещё. И уже к их компоненту Security программируют правила доступа на основе ролей/разрешений и поля authorId статьи.
Или войти через: