Написание и тестирование контроллеров

Написание контроллеров регистрации и их функциональное тестирование. Добавление локальных фикстур для тестовых сценариев. Проверка отправки электронных писем.

Скрытый контент
Комментарии (19)
Bondarenko Alexandr

Спасибо за урок, Дмитрий!

У меня есть вопросы:

1) Не удобнее было бы в тестах использовать отдельную базу данных для тестового окружения? Тогда после тестов не пришлось бы опять накатывать фикстуры.

2) При использовании контроллера вне изолированных контекстов бывают сценарии, когда есть необходимость вызвать несколько интеракторов из разных контекстов (по событию или же через "оркестратор"). Есть ли один из них при этом успешно модифицировал данные, а следующий выбросил исключение, следует откатить действие первого. Планируется ли рассмотреть подобный сценарий?

3) Планируется ли рассмотреть Graphql для озвученной в видео возможности чтения запрашиваемых клиентом дополнительных данных у эндпойнта?

Ответить
Дмитрий Елисеев

Не удобнее было бы в тестах использовать отдельную базу данных для тестового окружения? Тогда после тестов не пришлось бы опять накатывать фикстуры.

Да, есть альтернативы работы с той же БД:

1) Вызывать $em->getConnection()->beginTransaction() в методе setUp() и вызывать rollback() в tearDown(). Тогда тест будет выполняться в транзакции и после теста все данные будут восстанавливаться назад.

2) Подключить ещё один сервис api-postgres-test и указать его в тестовом окружении. И дополнительно накатывать миграции ещё и на неё.

Эти способы сработают только для кода внутри нашего внутреннего теста.

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

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

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

Ответить
Андрей

Я придумал еще третий вариант)

Можно перед запуском тестов делать дамп базы, а по окончании тестов восстанавливать его. Я вижу 2 преимущества в этом подходе:

  1. Это гораздо быстрее (хотя может и нет) подхода с транзакциями (как я понял транзакция стартует при выполнении каждого теста. Если данных в базе перед началом тестирования очень много, то каждый раз ее чистить/восстанавливать будет очень затратно по времени).
  2. Это удобнее чем подход с загрузкой фикстур после тестов, т.к. в фикстурах могут быть сгенерированные, фейковые данные. Т.е. по сути после загрузки фикстур база не будет в точности такой же как до начала тестов.

Как вам такой способ?)

Ответить
Дмитрий Елисеев

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

Да, это паттерн "Сага". Его рассмотрим.

Ответить
Дмитрий Елисеев

Планируется ли рассмотреть GraphQL для озвученной в видео возможности чтения запрашиваемых клиентом дополнительных данных у эндпойнта?

Расскажем, что универсальные решения удобны для CRUD-подхода, когда API является отражением один-в-один таблиц в БД. Но если делаем что-то кастомное, то проявляются неудобства.

Так что вдальнейшем сравним подходы к написанию API и выберем оптимальный вариант.

Ответить
Bondarenko Alexandr

Спасибо за ответы!

Ответить
Arunas

пересмотрел -> большое спасибо.

Ответить
Denis

Попробую сформулировать свой вопрос.

Допустим, что у нас появилась необходимость из модуля блог дернуть авторизацию. То как то правильно сделать, чтобы не напрямую дергать код из авторизации? Чтобы модули не были напрямую зависимы друг от друга.

Надо как то дергать контроллер? Надо вводить какую - то абстракцию для этого?

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

Ответить
Дмитрий Елисеев

Обычно авторизация производится на уровне контроллеров. При нашем подходе с отдельными контроллерами проблем с не будет. А роль пользователя для привязки прав будем передавать напрямую в токене аутентификации.

Ответить
Igor

Спасибо за уроки! Появилось два вопроса:

1) Если представить такую ситуацию. Нам необходимо вынести модуль Auth на отдельный сервер, когда производится аутентификация через OAuth2, токены для пользователя записываются в БД, которая отдельная/своя для модуля Auth. А как пользователь с этим токеном сможет достучаться до модуля Auction, если у него другая БД? Нам нужно при получении пользователем токена в микросервисе Auth отправлять его еще и в микросервис Auction и остальные микросервисы, лежащие на разных серверах с разными БД например через очередь? Или для такого используют другой подход?

2) Должен ли модуль Auth знать о доменных ролях? На например пользователь регистрируется на сайте и может иметь роль автора блога, либо читателя блогов и при регистрации он должен указать в качестве кого он хочет зарегистрироваться. Или модуль Auth будет иметь только роли ROLE_ADMIN, ROLE_USER. А роли ROLE_AUTHOR, ROLE_READER будут уже внутри модуля Blog, и будут выступать в виде сущностей Author и Reader?

Ответить
Дмитрий Елисеев

1) Если представить такую ситуацию...

Для этого используются не простые токены, а JWT, которые представляют собой зашифрованный JSON с идентификатором и ролью пользователя. Тогда Auction напрямую берёт user id прямо из токена и никуда больше не ходит.

Ответить
Дмитрий Елисеев

2) Должен ли модуль Auth знать о доменных ролях?..

Второй вариант, когда каждый модуль хранит свои сущности вроде Author и Reader. В этом случае доменные AUTHOR, READER можно делать не ролями, а флагами вроде is_author или наличием/отсутствием этих Author и Reader.

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

Поэтому общие роли можно хранить в Auth, а разрешения уже привязывать и проверять на уровне Http.

Ответить
Igor

Спасибо за ответы, Дмитрий!

Ответить
Андрей

Добрый день, Дмитрий!

Подскажите, пожалуйста, как лучше реализовать собственные обработчики ошибок 404 и 405 взамен тех, что существуют в Slim по умолчанию и возвращают ответ в формате HTML. В частности, необходимо на любые запросы к API (правильные или ошибочные) всегда возвращать свой ответ в формате JSON.

Ответить
Андрей

В итоге добавил вызов forceContentType('application/json') и написал свой JsonErrorRenderer, выводящий ответ в нужном виде.

Ответить
Дмитрий Елисеев

Достаточно переопределить обработчик по умолчанию с html на json:

$errorHandler->setDefaultErrorRenderer(
    'application/json',
    JsonErrorRenderer::class
);
Ответить
Сергей

Не могу понять, мне выдает что

[2020-10-03T07:44:30.370415+00:00] API.ERROR: Entry "App\Http\Action\RequestAction" cannot be resolved: Entry "App\Auth\Command\JoinByEmail\Request\Handler" cannot be resolved: the class is not instantiable

routes:

return static function (App $app): void {
    $app->get('/', HomeAction::class);
    $app->post('/v1/auth/join', RequestAction::class);
};

Handler.php

class Handler
{
    private UserRepository $users;
    private PasswordHasher $hasher;
    private Tokenizer $tokenizer;
    private Flusher $flusher;
    private JoinConfirmationSender $sender;

    private function __construct(
        UserRepository $users,
        PasswordHasher $hasher,
        Tokenizer $tokenizer,
        Flusher $flusher,
        JoinConfirmationSender $sender
    ) {
        $this->users = $users;
        $this->hasher = $hasher;
        $this->tokenizer = $tokenizer;
        $this->flusher = $flusher;
        $this->sender = $sender;
    }

    public function handle(Command $command): void
    {
        ...
    }
}

Не понятно, что в нём не может зарезолвится, помогите пожалуйста

Ответить
Дмитрий Елисеев

Вы объявили конструктор приватным.

Ответить
Сергей

Да, я заметил потом, прошу прощения, забыл отписаться. Благодарю!

Ответить
Зарегистрируйтесь или войдите чтобы оставить комментарий