В эти дни проходит внутренняя конференция Aventus IT. И я там был, мёд пиво пил… Шучу. Выступал на тему автоматизации контроля качества кода. И хочу представить вам статью по материалам своего выступления.

Сегодня было два прекрасных доклада про тесты на проекте. Порадовало, что услышал в них некоторые свои идеи. Например, про преимущества запуска тестов на CI вместо локальной машины. Оказывается, что я не один такой.

Со своей стороны, хочу поговорить, про контроль качества кода помимо тестов. Постарался сделать доклад максимально практичным. Впрочем, не без необходимой лирики. В статье начну с каких-то может быть банальных вещей, от которых мы перейдём к менее очевидным и, вместе с тем, важным вещам.

Мотивация автоматизации

Для начала разберемся для чего это нужно.

У программного продукта есть заказчик, поэтому в первую очередь ответим, что нужно бизнесу: бизнесу необходима система выполняющая бизнес задачи. И одно из требований бизнеса ー эта система должна работать стабильно. Бизнесу по сути всё равно сколько костылей мы туда вставили. Ему не до красоты кода. Главное, что его интересует, ー система должна работать… И при минимально возможных издержках. И вот тут у нас есть точки соприкосновения.

Как мы можем обеспечить качество программного продукта? Всё начинается с ручного контроля:

  • Code review

  • Manual QA

Это требует много времени. Соответствующих зарплат. Так Code Review в идеале должен проводить более сильный специалист, чем тот кто писал код. Если мы не говорим про обучающую функцию cross-review, то основная задача ревью ー это найти ошибки и слабые стороны в предложенном решении. То же касается и QA. Они неоправданно недооценены. В идеале, QA специалист должен понимать логику работы системы лучше, чем разработчик! Ведь он контролирует разработчика. На это нужна соответствующая квалификация. И зарплата. Это дорого!

Что будет если автоматизировать проверку кода. И сейчас речь не про тесты, которые живые и которые постоянно должны меняться вместе с изменением проекта. Существует множество вещей, которые нужно один раз настроить и далее всё время получать профит. Остаётся только оплатить серверное время для раннера на CI. Сколько стоит месяц машинного времени, выделенное под runner на CI на проекте? 50-100-200-300-500 долларов? Тут команда саппорта может дать более точные цифры. Конечно это не все затраты, тем не менее, это сильно дешевле, чем время 2-3-х сотрудников (QA + dev). Возьмём оценочно 5000$ на всех участников процесса.

Теперь возьмите разницу этих цифр и умножьте на 12 месяцев и хотя бы на 2-3 года. И посчитайте на 10 лет. Многие проекты живут ещё дольше. Цифра впечатляет.

Что настраиваем

Будучи лидом, в первую очередь, я начал экономить своё время на код ревью. И поставил соответствующие скрипты у себя на проекте.

PHP CS Fixer (проверка стиля кода) 

Начинаем с проверки стиля кода.

Почему это важно? Хоть код мы пишем условно один раз, но читаем код много-много раз. И чем быстрее разработчик разберется в чужом коде, тем быстрее он выполнит свою задачу. Для этого код разных разработчиков должен выглядеть максимально одинаковым. 

Есть мнение:

Поддерживаемость кода, в том числе читабельность, важнее оптимальности алгоритмов!

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

Во-вторых, большая часть любого проекта не является высоконагруженной. И если мы столкнемся с проблемой нагрузки, то всегда можно докинуть железа и после ТОЧЕЧНО оптимизировать производительность. Так общая стоимость создания и владения системой будет ниже.

Тут важно придерживаться стандартов. Проект на Symfony ー значит включаем правила, принятые в компонентах Symfony. Так сразу снимается вопрос “а мне кажется, что так красивее”. И когда все проекты придерживаются стандартного подхода, то снижается порог входа новых разработчиков в проект.

Как упростить внедрение в проект?

  1. Посмотреть какие ошибки встречаются не часто и исправить. Фиксер умеет делать это автоматически.

  2. Временно отключить часть правил, которые исправить сразу сложно.

  3. Использовать baseline. Из коробки фиксер такое не умеет, поэтому я создал пакет, который обеспечивает такую функциональность: https://github.com/Aeliot-Tm/php-cs-fixer-baseline. Он позволяет мягко отключить проверку части файлов до тех пор, пока они не будут модифицированы. Это особенно полезно на проектах с большой кодовой базой. Дополнительно, получаем ускорение проверки на CI за счёт того, что проверяются только новые файлы и модифицированные.

Code sniffer

squizlabs/php_codesniffer

Альтернатива предыдущему скрипту проверки стилей. Используем на проекте, т.к. Есть полезные правила, которых нет в фиксире. Нарпимер, длина строки.

PHPStan (статический анализ кода)

Он ищет в коде ошибки в логике. Например, несоответствие типов данных в операциях. Например, когда мы сравниваем холодное с мягким)) Null-pointer и многое другое.

Важный параметр: “level”  от 1 до 8 ー это что-то типа набора пресетов правил. Чем больше цифра ー тем больше правил включено.

Имеет множество плагинов. Мы используем:

  • phpstan/phpstan-doctrine - проверяет DQL, определяет возвращаемые типы магических методов репозиториев и др.

  • phpstan/phpstan-symfony - позволяет правильно определять типы возвращаемые из контейнера и не только.

  • phpstan/phpstan-phpunit - улучшает проверку моков.

Их можно подключать как вручную в основном конфиге, так и с помощью phpstan/extension-installer.

Есть ещё тонкость с их настройкой.

Для phpstan/phpstan-doctrine необходимо предоставить файл, возвращающий ObjectManager.

doctrine:
   objectManagerLoader: ../../tests/object-manager.php
   allowNullablePropertyForRequiredField: true

Для phpstan/phpstan-symfony необходим загрузчик приложения и путь к xml файлу кэша контейнера.

symfony:
   console_application_loader: ../../tests/console-application.php
   container_xml_path: ../../var/cache/test/App_KernelTestDebugContainer.xml

Линтер YAML файлов

На проекте много YAML файлов. Так что без линтера никуда. Мы используем консольную утилиту yamllint. И чтобы не тратить время на её установку мы собрали свой docker-контейнер.

Линтер TWIG файлов

Если что-либо рендерите из twig файлов, то не забываем проверить их синтаксис линтером:

bin/console lint:twig

Проверка консистентности файлов переводов

Используется мультиязычность? Как проверить? 

Есть стандартная команда Symfony, которая закрывает часть потребностей:

bin/console debug:translation --all uk

Но она может не всё. Например, не сравнивает несколько языков между собой в поисках пропущенных значений. Кроме того, на более старых версиях Symfony она была совершенно не пригодна для CI, т.к. не отдавала кодов ошибок при их обнаружении.

Если переводы в YAML файлах, а как правило именно в них, то ставим пакет https://github.com/Aeliot-Tm/translation-maintain, и радуемся жизни. Сам когда-то делал его. И настраиваем команду на CI:

bin/console aeliot_trans_maintain:lint:yaml base file_transformed

Она имеет разные готовые правила и их наборы. Дополнительной плюшкой будет то, что пакет предоставляет ряд дополнительных возможностей, по управлению файлами переводов на этапе разработки. Включая нормализацию структуры файлов и автоматический перевод, пропущенных значений на нужный язык. Подробнее можно почитать в статье на хабре: https://habr.com/ru/articles/555954/

Линтер контейнера

В большинстве Symfony проектов конфигурационные файлы в YAML формате. Линтер YAML файлов подсветит, только синтаксические ошибки. Чтобы проверить логические ошибки в определении параметров и сервисов нужен линтер контейнера. И он есть в Symfony из коробки:

bin/console lint:container

Проверка схемы базы данных

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

  • bin/console doctrine:migrations:diff -n

  • bin/console make:migration -n

Можно встретить использование doctrine:migrations:generate и ручное добавление инструкций SQL. Или разработчик может забыть создать миграцию или не сохранить её. Или поработать в одной ветке, выполнить миграции. После переключиться в другую и там создать новую. При этом забыв откатить предыдущую миграцию. После этого он пойдёт удалять лишние инструкции из новой миграции и пожет удалить что-то нужное. 

Со временем у вас почти наверняка появится рассинхрон между структурой базы данных, описанной в энтитях и реальной структурой базы данных. Как не пропустить это на прод? Как вы уже поняли, настраиваем команду на CI:

bin/console doctrine:schema:validate -n -v

Здесь два аргумента отвечают за вывод более подробной информации об ошибке и отключение интерактивного режима для корректной работы на CI.

Что если у вас в базе данных есть таблицы, неуправляемые доктриной? Нужно настроить фильтрацию и дальше Доктрина сделает всё, что нужно!

PHPUnit

Как же без него. Не буду глубоко погружаться. Отмечу только несколько моментов:

Во-первых, final классы можно прекрасно мокать. Нужен только пакет dg/bypass-finals. Дальше одна строчка в bootstrap файле и полетели.

BypassFinals::enable();

Спорить здесь о нужности или ненужности инструкции final сейчас будем. Как и обсуждать создание one-used интерфейсов только ради тестов (читай тестового кода в продовом). Просто примите тот факт, что мокать их можно.

Не бесплатно, конечно. PHPStorm будет ругаться и PHPStan будет недовольным (baseline в помощь), но работать будет.

Во-вторых, оборачиваем все операции в базе данных в транзакции и откатываем изменения автоматически с помощью dama/doctrine-test-bundle. Просто установите пакет и добавьте экстеншен в конфиги. И вам не нужно будет следить за этим на сетапе тестов и tearDown.

<extensions>
​ <extension class="DAMA\DoctrineTestBundle\PHPUnit\PHPUnitExtension"/>​
</extensions>

В-третьих, если есть желание поиграть в контроль покрытия тестами, то понадобится дополнительный пакет https://github.com/Aeliot-Tm/phpunit-codecoverage-baseline, т.к. PHPUnit из коробки умеет посчитать покрытие тестами, но не умеет сравнивать предыдущее состояние и новое.

Проверка composer-файлов

Помним про самоконтроль. Собственный файлы композера тоже следует проверять, т.к. разработчики тоже люди и могут забыть закомментировать часть изменений. Собственная команда композера поможет проконтролировать это:

composer validate --strict

Следим за неиспользуемыми зависимостями

Я придерживаюсь политики, что неработающий код нужно удалять. От источник багов и на чтение его впустую тратится время. То же самое касается и пакетов (библиотек). В процессе жизненного цикла проекта мы можем начать использовать какие-то библиотеки и потом прекратить их использование. И не всегда разработчик может проконтролировать и вовремя удалить зависимость в файлах композера. Поэтому, устанавливаем пакет icanhazstring/composer-unused. Он позволяет контролировать не только установленные пакеты, но и требования наличию в системе тех или иных модулей PHP.

Явное добавление зависимостей

Чтобы не попасть в просак и не лишиться используемого компонента при обновлении пакетов. Устанавливаем пакет https://github.com/maglnet/ComposerRequireChecker

Если этот пакет не стоит и не настроен, то любое обновление пакетов - это как ходить босиком по минному полю с ружьём, приставленным к виску. При поиске решения для бизнес задачи разработчик может взять установленный пакет, но установленный как неявная зависимость. И после обновления этот пакет может пропасть из системы. И хорошо, если проект сразу сломается. А что если зависимость использовалась в части проекта не покрытого тестами? Тут радостные баги кричат: “Пользователи, мы идём к вам” ?

Security vulnerabilities check of composer

Ещё на этапе установки необходимо проверять пакеты на наличие известных уязвимостей. Для этого устанавливаем пакет в dev-зависимости: roave/security-advisories

Но этого недостаточно. Нужно проверяем используемые зависимости на наличие известных уязвимостей уже после установки. Для этого устанавливаем пакет: enlightn/security-checker 

И настраиваем скрипт:

vendor/bin/security-checker security:check composer.lock

Scan container on vulnerabilities

Проверяем безопасность компонентов Docker контейнера с помощью утилиты trivy. Делаем это при сборке базового основного. И далее по расписанию или дополнительно в основном проекте, т.к. От всегда перед глазами.

https://github.com/aquasecurity/trivy

Создание тикетов по TODO-комментариям в коде

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

Обычно как? Разработчик добавил TODO в коде и только он знает о ней. Или того хуже, когда на проекте запрещают добавлять такие комментарии с словами или исправь сразу или создай нормальный тикет. Тикет создавать долго. Поэтому разработчик ничего не делает с этим. 

И потом Лид приходит к бизнесу и просит выделить время на исправление какого-то эфемерного тех. долга. И на вопрос “сколько его там” никто не может ответить, поскольку его никто не считал. 

И снова мой пакет: https://github.com/Aeliot-Tm/todo-registrar

Так по каждой проблеме появляется тикет в JIRA (или Github issues) и попадает на оценку на груминге. И дальше с ним происходит магия менеджмента. Это сильно упрощает управление тех. долгом.

Есть статья на хабре, где всё подробно описано https://habr.com/ru/articles/832994/

Следим за удалением устаревших TODO-шек

Ставим пакет: https://github.com/staabm/phpstan-todo-by

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

Автоматизация контроля архитектурных решений

Есть два пути:

  1. Кастомные правила для PHPStan

  2. Установка пакета PHPAT Который по сути является надстройкой над PHPStan и предоставляет упрощенный интерфейс для настройки собственных правил.

Итоги раздела

И это ещё не всё, что можно настроить на CI! Существует мнение:

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

Хорошие практики в настройке скриптов

Создаём единую точку настройки для dev-tools

В идеале на локальной машине скрипты должны запускаться так же как и на CI. Можно дать инструкцию, но длинная команда с множеством параметров может быть неудобной для локального использования. К тому же, в процессе жизненного цикла разработки приложения может понадобиться скорректировать настройки скриптов, что-то куда-то перенести. Прибавьте сюда работу в разных версиях (ветках). И тут напрашивается создание единой точки входа. После ряда экспериментов выбор пал на composer-скрипты. Так мы имеем единую точку входа и для CI и для локальной разработки. С неизменным перечнем команд в разных версиях кода. + Make файл для упрощения вызова извне Docker контейнера.

Так, например, когда число конфигов dev-скриптов в корне проекта стало превышать разумные разделы и мы стали переносить их в отдельную папку. Для разработчиков ничего не поменялось. Потом мы стали переходит на использование PHAR файлов, для установки тех же скриптов вместо композера. И тоже для разработчика ничего не поменялось. Он как вызывал команду composer cs-fix, так и продолжил. Вне зависимости в какой ветке он сейчас находится.

Выносим конфиги для dev-скриптов в отдельную папку

Большинство скриптов по умолчанию рассчитывают на наличие конфига в корне проекта. Это позволяет упростить вызов скрипта и не передавать путь к нему. Со временем конфигов становится много. Особенно, когда приходится делить скрипты на несколько вызовов с разными настройками для ускорения пайплайнов. У нас в проекте это папка scripts с подпапками внутри.

Используем “наследование” в настройке джоб

Синтаксис YAML файла, поддерживаемый Gitlab CI позволяет вынести общие свойства джоб в отдельный блок и наследоваться от него. Это сильно улучшает читабельность кода и снижает нагрузку на его поддержку из-за уменьшения копипасты. После этого добавление новой джобы превращается в пару строк:

'Composer check obvious requiring of packages':
   <<: *run_tests
   script:
       - composer require-check


'Composer check unused dependencies':
   <<: *run_tests
   script:
       - composer unused -- --output-format=gitlab


'Doctrine database Schema Validation':
   <<: *run_tests_integration
   script:
       - composer db-validate

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

Снижаем dependency-hell с помощью PHAR/Phive

Каждый dev-script может привнести свои зависимости. Чем больше зависимостей в composer.json файле, тем труднее обновлять их до новых версий. В итоге, это может приводить к невозможности установки тех или иных компонентов.  Эта проблема решается с помощью установки dev-скриптов в виде PHAR файлов. Проще всего это сделать с помощью Phive. Это что-то типа composer только для установки PHAR-файлов.

Важное замечание! По умолчанию Phive устанавливает пакеты в проект в виде ссылок (symlink) и без расширения .phar. В этом случае PHPStorm не понимает какое содержимое у этого файла и не работает подсветка синтаксиса в конфигах скриптов. Поэтому, я рекомендую всегда исправлять параметры установки пакетов в phive.xml файле.

Скорость прохождения пайплайнов на CI

Очень важно как быстро отрабатывают ваши пайплайна. Как долго они могут выполняться и какое время считается нормальным? 

Встречал в литературе статистический анализ говорящий, что допустимым является 15 минут на выполнение пайплайна. Я бы сказал, что допустимым является не более полутора минут. И у меня есть аргументы:

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

Во-вторых, хватит мучать разработчиков локальной прогонкой проверок! Объективно, не у всех ребят очень мощные компьютеры. И не всегда имеется возможность предоставления соответствующего оборудования. Прогонка всех проверок локально может проходить долго. И в это время разработчик не может ничего делать. Когда проверки на CI проходят быстро, то становится удобнее проверять код на сервере. Это быстро. К тому же, запушил код и сразу можно заняться чем-то ещё полезным. Имеем выигрыш в 10-30 минут на каждом пуше. И за год набегает солидный запас. Плюс, имеем хорошую привычку всегда пушить на сервер. Что дает подстраховку сохранности кода и оперативный доступа к нему всей команды.

В-третьих, конкуренция за ресурсы. Чем больше разработчиков одновременно пушат код в репозиторий, тем дольше придётся ждать, пока освободятся ресурсы раннера на запуск именно твоих тестов. Это важно при массовом пуше разработчиков. Если Ваш ранер отрабоатывает 30 параллельных джоб. И в одном пайплайне их 10. То одстаточно 3-м разработчика запушить код, и все остальные будут здать своей очереди. В итоге, 15 минут на пайплай превращаются в 30 или 45 минут. Если же ваш майплай выполняется за минуту, то если он будет выполняться 2-3 минуты, то это никто особо не почувствует.

В-четвертых, снижение затрат. Чем быстрее проходят проверки, тем меньше нужно выделять ресурсов для раннеров. И меньше простоев разработчиков из-за ожидания завершения автоматических проверок.

Оптимизируем сборку контейнера

Первое, что нужно сделать ー оптимизировать сборку контейнера. Сейчас не пойдём в детали, а только рассмотрим общий план оптимизации.

Рассмотрим на примере php.

В первую очередь, так называемая послойная сборка. Хорошо ускоряет пайплайны за счёт кэширования слоёв. 

Следующий этап ー разделение образа на части.

  1. Установка базовых модулей (все используемые на проде. до копирования кода в него) 

  2. Dev-слой. Установка таких вещей, как xDebug и прочее. Отдельный образ на основе предыдущего. 

  3. Установка вендоров с/без dev

Первых два, как редко изменяемые, выносим в отдельный репозиторий и версионируем в ручную. 3й ー автоматически версионируем по хешу composer.json и phive.xml. И храним 3й слои в основном репозитории. В двух версиях: с дев зависимостями и без них. Версию 3-го слоя меняем в коде и коммитим в ту же бранчу автоматически в том же пайплайне. Может быть немного заморочено, зато время сборки контейнера для большинства мердж-реквестов составляет считанные секунды.

Так в большинстве случаев мы используем готовый контейнер и не тратим время на его сборку.

Аналогично с другими контейнерами. Везде, где управляете модулями и глобальными настройками. 

Прогреваем кэшь один раз для всех джоб пайплайна

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

'Build Common Artifacts':
    stage: build-artifacts
    image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    artifacts:
        untracked: true
        expire_in: 7 days
    script:
        - bin/decode_files "." "${IV}" "${KEY}"
        - bin/console cache:clear
    variables:
        APP_ENV: "test"

Далее, в базовом шаблоке для джоб тестов прописываем зависимость от этой джобы:

.run_tests: &run_tests
    stage: tests
    image: $CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA
    needs:
        - job: 'Build Common Artifacts'
          artifacts: true

Помечаем джобы как прирываемые

В GitLab есть замечательная инструкция для джоб

interruptible: true

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

Дробим джобы и параллелим

  • Дробим тесты на пресеты

  • Parallels ー шикарная утилита. Особенно для тестов в комбинации с Paratest. Позволяет запускать каждый отдельный тест в параллельных процессах. Это ещё сильнее сокращает общее время пайплайна.

  • Не смотря на то, что PHPStan работает в параллельных потоках по числу доступных ядер процессора. Этого бывает недостаточно. Дробим одну джобу PHPStan на несколько для проверки параллельно разных правил и возможно разных разделов проекта. Поэтому разбиваем на разные джобы. Он позволяет тонко настраивать в каких парках проверять код, а в каких только сканировать для правильного анализа использованных зависимостей.

Комментарии (0)