Автоматизированное тестирование при разработке приложений — чрезвычайно важная практика. В процессе коммерческой разработки сайтов с бэкендом на PHP, автотесты — скорее добрая воля со стороны программиста и понимание процессов со стороны менеджера, чем общепринятая практика: заказчику зачастую неясно, на что программист тратит 20% рабочего времени и что это за автотесты вообще такие.

— … отладка, тестирование, исправление ошибок.
— Пиши сразу без ошибок!

В последнее время, однако, эта ситуация поменялась, и мы уже не натыкаемся на недоуменные взгляды при фразе «покрытие кода тестами» — уровень заказчиков (а вместе с ним и понимание процессов со стороны заказчиков) растет.

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

В своей работе мы используем Codeception — замечательный, гибкий и современный фреймворк для тестирования php-приложений. Есть, однако, некоторые сложности (особенно для начинающих разработчиков), особенно потомоу, что в официальных руководствах обычно приводятся примеры для PHPUnit, расширенного и дополненного авторами фреймворка.


Проблема, с которой столкнулся один из наших программистов

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

Каким образом в Symfony 4 авторизовать не живого пользователя, а клиента, который действует внутри функционального теста codeception?

Официальное руководство Symfony говорит:

  • создайте отдельный механизм http-аутентификации для тестов, или
  • создайте внутри теста отдельный метод, который будет реализовывать подстановку авторизационного токена для сессии Symfony\Component\BrowserKit\Client

В целом сложностей быть не должно — модуль Symfony для Codeception реализует именно этот компонент, то есть добравшись внутри теста до свойств клиента мы легко подставляем ему UsernamePasswordToken.

Сложность в том, что внутри функционального теста экземпляр клиента недоступен, поэтому и подставить ему cookie нельзя. HTTP-авторизацию использовать тоже нельзя — запись клиента в БД содержит множество нужных ему свойств, просто http-заголовок не пойдет, а идею «Авторизоваться тестом через форму, сграбить сессию и cookies и потом использовать» мы, по понятным причинам, отбросили сразу же.

Решение проблемы

оказалось довольно тривиальным: нужно использовать встроенный в Codeception механизм хелперов, внутри которых доступны модули и их свойства, объявить метод, который будет давать клиенту авторизацию и будет использоваться внутри теста.

В файле tests/_support/Helper/Functional.php объявляем метод:

Как видно из кода, мы практически полностью следуем официальному руководству в части установки токена, но предварительно создаем в БД запись для тестового пользователя и получаем, собственно, объект пользователя. Все сервисы, нужные для шифрования пароля и формирования токена, могут быть получены через метод grabServiceкомпонента Symfony.

В результате, внутри теста:

Возможные проблемы, которые могут возникнуть при использовании этого способа:

  • в вашей базе данных уже может быть пользователь с такими реквизитами. Это ваша ошибка — создавайте базу данных для тестов отдельно, пустую, и накатывайте туда миграции, чтобы не нарушать чистоту тестовой среды;
  • может (по различным причинам) не пройти создание пользователя в БД но — если причина в классе пользователя или репозитории, это должны показать unit-тесты, другие причины (падение БД, интерпретатора, контейнера) — вопрос отдельного исследования;
  • этот способ, конечно, занимает значительное (по сравнению с http-авторизацией) время, но такого рода потери неизбежны, если свойства пользователя включают в себя нечто большее, чем имя и пароль. В качестве решения можно предложить задействовать метод _beforeSuite хелпера, и выполнять создание пользователя перед началом всего комплекта тестов, а не каждый раз.

Надеюсь, эта статья поможет кому-то сэкономить пару часов рабочего времени :)

Happy development!