Легкий способ начать писать тестируемый и поддерживаемый код на PHP

Что мне не нравится в php — это то, что язык позволяет легко написать неподдерживаемую ересь, которая работает. Может, кто-то скажет — работает же, зачем это трогать? Затем, что иногда приложение может меняться и приходится это править. Тогда то Вы и почувствуете всю печаль ситуации — многие просто игнорируют стандарты psr-1 и psr-2, игнорируют существование composer и вместо использования сторонних протестированных библиотек продолжают писать свои велосипеды, которые работают только в данной ситуации и будут ломаться, если им подсунуть что-то еще.

Так что с этим делать? Пора научиться пользоваться современными инструментами.

Есть несколько вариантов исправления ситуации — использовать фреймворки или же собрать свое приложение из сторонних компонентов по стандартам. В этой статье мы рассмотрим второй вариант, т.к. у фреймворков есть своя документация, следуя которой и нужно создавать свое приложение. Итак, далее я вкратце расскажу о некоторых принципах, которые позволят Вам собрать тестируемое, расширяемое приложение. 

Используем composer

Composer — это менеджер зависимостей для PHP. С его помощью Вы можете быстро и безболезненно управлять зависимостями в своем проекте, добавлять сторонние библиотеки, управлять автозагрузкой классов и много чего еще полезного. Давайте рассмотрим небольшой пример, возьму реальный кусок из одного проекта, которым занимаюсь в свободное от всего время

По пунктам названия и описания думаю все понятно. В секции autoload мы говорим, что будем использовать пространство имен Puzzle для файлов, находящихся в папке src/Puzzle. Это позволит нам автоматически загружать нужные классы, если их расположение и название соответствует стандарту psr-4. В секции require мы говорим, какие версии библиотек нам нужны и плавно переходим ко второму пункту

Используем сторонние компоненты

Если Ваше приложение не очень масштабное и cms или фреймворки для него избыточны, Вы вполне можете использовать или компоненты фреймворков, или просто отдельные библиотеки. Зачем это нужно:

  • Как правило, такие компоненты хорошо делают то, зачем они созданы и протестированы
  • не нужно изобретать велосипед и заново решать задачи, которые до Вас точно решили уже тысячи людей
  • поддержка сообщества и куча примеров использования

Насколько я знаю, есть несколько популярных фреймворков, компоненты которых Вы можете использовать отдельно

В приведенном выше composer.json используются компоненты Symfony и Aura, о чем и будет частично третий пункт

Инверсия управления

Для начала ознакомьтесь с материалом по ссылке. И теперь краткие примеры зачем это нужно.

Ранее в уроках  мы использовали такую конструкцию:

или такую

И что в этом плохого, скажете вы? В принципе ничего особенного, только вот если мы будем создавать тесты для класса Page, внезапно окажется, что зависимость от модели определена жестко, т.к. при тестировании нам придется иметь дело с реальными данными, которых может и не быть, т.к. если мы захотим протестировать, как ведет себя контроллер для страниц, нам нужно будет подключиться к базе, сделать запрос, впихнуть все это во View, и иметь отдельно тестовые данные, которые мы не сможем удалить. В общем тестирование данного класса будет крайне проблемным.

Как решить эту проблему? Для этого и существует такое понятие, как внедрение зависимости. Мы можем переписать это таким образом

И код базового контроллера, от которого мы наследуем контроллер для страниц

Что это изменило? Теперь наш контроллер для страниц сам по себе, и он не знает о том, с какой моделью ему нужно будет работать. Но все равно мы не сможет ему подсунуть какую-нибудь дичь, т.к. мы используем контроль типов и наш класс будет работать только с инстансом \Puzzle\Models\PagesModel — или с самим, или с расширенным. В своем приложении Вы можете использовать для этого интерфейсы, чтоб в ваши классы могли передать только зависимости, которые реализуют нужный функционал.  Еще мы выбрасываем исключение,  когда у контроллера страниц пытаются запросить то, что он отдать не может. Теперь следующий пункт

Использование контейнера зависимостей

В предыдущем пункте мы избавили контроллер от необходимости знать, с какой моделью ему работать. Но как нам сказать ему о том, что ему нужно работать именно с той моделью? Для этого мы в примере будем использовать компонент Aura.DI. Почему его? Он быстрый, отлично документирован и простой. Следуя документации, давайте определим зависимости для нашего контроллера

Это просто пример, в котором мы говорим, что задаем зависимость для класса Puzzle\Controllers\PagesController через сеттер setModel, а в родительский контроллер мы внедрили общий сервис Response и View. Зачем все это? Таким образом мы сможем быстро и безболезненно сменить к примеру движок шаблонизации с нашего самописного на Twig, а те зависимости, которые мы внедрили в родительский объект, останутся и в дочернем (Response и View). Таким образом в теории мы реализовали принцип инверсии управления и наш контроллер для страниц стал независимой единицей и ему по бую тонкости реализации механизма работы с http и шаблонизацию, он просто делаем свой маленький кусок работы.

Создаем юнит-тесты для нашего контроллера

Я использую netbeans в качестве ide для разработки, так что через него можно создать файл bootstrap для тестирования с таким содержимым

Это нужно чтоб наши тесты смогли так же подгружать нужные классы. Для тестирования мы будем использовать phpunit. Документация по использованию вместе с netbeans. В рунете не особо много материалов по тестированию в php, поэтому сейчас я попробую вкратце описать наиболее трудные для начинающих вопросы (по моей субъективной оценке). В общем пример юнит-теста для нашего контроллера страниц

Вот такой достаточно объемный юнит-тест для такого маленького класса. Многие возмутятся — ну и зачем мне писать юнит-тесты, если они по объему больше основного кода? Это не совсем так, больше по объему здесь получилось, потому что мы используем моки и стабы, подробнее о них ниже, а сейчас давайте разберем что это и зачем мы это написали.

Сначала мы создаем тестируемый объект в методе setUp. Т.к. мы использовали внедрение зависимости, теперь мы можем подменить зависимости, чтоб проверить работу контроллера и не дергать реальную базу данных. В строке

Мы вредряем реальный объект Symfony\Component\HttpFoundation\Response, потому что его использование не требует наличия каких-либо особенных тестовых данных и не замедлит выполнение теста.

Для свойства view нашего контроллера мы используем заглушку, которую мы получаем в методе getView

Почему здесь можно использовать заглушку? Потому что для тестирования нашего контроллера нам не нужно знать как именно там создался шаблон и т.п.. Все что нам нужно — имитировать его создание, т.к. мы следуем принципу разделения ответственности и контроллер не должен знать ничего о рендеринге шаблона.

А для свойства model мы будем использовать mock-объект, потому что хоть контроллер и не знает каким образом модель получает данные, они ему нужны. Поэтому мы имитируем получение данных из базы, чтоб контроллеру было с чем работать

Обратите внимание на

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

В методе testGetPage2  мы тестируем, что наш контроллер выбросит исключение, когда мы подсунем ему не то, что нужно

И самое важное

аннотация @dataProvider говорит нам, откуда брать данные, чтоб протестировать выполнение контроллера с нужными данными

первый аргумент — это slug страницы, которую мы получаем. В первом случае  наш контроллер должен выдать код ответа 404, т.к. мы не определили его в модели, а в остальных случаях — 200, т.к. мы можем получить нужные данные. И заодно мы проверяем, что нам вернули объект Symfony\Component\HttpFoundation\Response, т.е. запрос отработался как нужно.

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

Итоги

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

  • Следуйте стандартам psr-1,psr-2,psr-4 — так любой человек сможет прочитать без труда, что Вы написали
  • Используйте инверсию контроля — это сделаем Ваш код гибким и тестируемым, и позволит использовать компоненты во многих проектах, не привязываясь жестко к определенному шаблонизатору, СУБД и т.п.
  • Пишите юнит-тесты — когда разберетесь, то это окажется очень просто и сэкономит кучу времени на отладке, сопровождении и добавлении нового функционала.

Спасибо за внимание, если есть вопросы — пишите в комментарии


Добавить комментарий

Войти с помощью: 

Комментарии:

  • Оставьте первый комментарий - автор старался

Вход на сайт
Разделы
Метки
wordpress статьи расширение функционала php бред конкурс бесполезная информация халява скрипты психология лирика это Россия баян администрирование комментарии