Беги, PHPUnit, беги: как я оптимизировал время выполнения тестов

от автора

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

Как я с этим боролся и что получилось в итоге?

Что было до оптимизаций

Приложение разрабатывается на PHP 7.2 и Symfony 4.2. Для написания тестов пользуемся PHPUnit. Большая часть тестов представляет собой интеграционные тесты для REST API.

Запускаем тесты:

> bin/phpunit ... Time: 20.17 minutes, Memory: 400.25 MB OK (1494 tests, 5536 assertions)

На все тесты ушло около 20 минут. Это приводило к следующим проблемам:

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

Начинаем искать основные узкие места и экспериментировать!

DAMADoctrineTestBundle

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

С помощью специального PHPUnit Listener перед запуском каждого теста открывается новая транзакция и откатывается после завершения теста. Также библиотека предоставляет средство кэширования метаданных и запросов для всех EntityManager. Пример конфигурации:

dama_doctrine_test:     enable_static_connection: true     enable_static_meta_data_cache: true     enable_static_query_cache: true

Мы уже использовали эту библиотеку, но если у вас её нет, то обязательно попробуйте.

Рекомендуемая конфигурация Symfony

У фреймворка Symfony прекрасная документация, в которой в частности есть раздел про производительность. Советую ознакомиться с ним и проверить вашу конфигурацию.

Если вы уже используете PHP 7.4 и последние версии Symfony, то обязательно попробуйте предварительную загрузку классов.

У нас уже были выставлены все рекомендуемые параметры OPcache и PHP, поэтому возникла идея обновиться до PHP версии 7.4 и Symfony 4.4, проверив будет ли прирост производительности в тестах, тем более это не должно было занять много времени.

PHP 7.4 + Symfony 4.4

Заодно обновим PHPUnit до текущей последней версии — 9.1.5.

Перед обновлением ознакомимся с документацией по переходу на новые минорные версии Symfony:

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

Вносим изменения в php.ini для dev, test и prod окружений:

;php.ini opcache.preload=/var/www/web/var/cache/dev/srcApp_KernelDevDebugContainer.preload.php

И services.yaml:

parameters:     container.dumper.inline_factories: true     container.dumper.inline_class_loader: true

Запускаем наши тесты и…

Time: 17:15.483, Memory: 451.00 MB OK (1494 tests, 5536 assertions)

Ускорились на 3 минуты, но нам этого мало.

XDebug

Для сборки и запуска приложения используется Docker с multistage сборкой. Для локальной отладки кода в dev сборку подключается PHP-расширение XDebug, которое также влияет на скорость выполнения кода. Во время прогона всех тестов отладка кода нам не нужна, поэтому для этого случая отключаем расширение с помощью небольшого скрипта (спасибо stackoverlow):

#!/bin/sh  php_no_xdebug() {   temporaryPath="$(mktemp -t php.XXXXXX).ini"    # Using awk to ensure that files ending without newlines do not lead to configuration error   php -i | grep "\.ini" | grep -o -e '\(/[a-z0-9._-]\+\)\+\.ini' | grep -v xdebug | xargs awk 'FNR==1{print ""}1' | grep -v xdebug >"$temporaryPath"    php -n -c "$temporaryPath" "$@"   rm -f "$temporaryPath" }  php_no_xdebug $@

Проверяем:

> php-no-xdebug.sh bin/phpunit Time: 14:11.561, Memory: 445.00 MB OK (1494 tests, 5536 assertions)

Отлично! Выиграли дополнительные 3 минуты.

Алгоритм хэширования паролей

Я уже упоминал, что большая часть тестов — интеграционные, которые начинаются с создания клиента:

protected function createAuthenticatedClient($username, $password): HttpKernelBrowser {     $client = static::createClient();     // Отправляем HTTP запрос на аутентификацию, получаем JWT токен и устанавливаем его для последующих запросов.     ...     $client->setServerParameter('HTTP_Authorization', sprintf('Bearer %s', $data['token']));     return $client; }

Посмотрим конфигурацию Symfony:

# config/packages/security.yaml security:     encoders:         App\Entity\User: bcrypt

Encoder описывает каким образом будут проверяться и храниться пользовательские пароли.
А теперь получим полную актуальную конфигурацию с учётом стандартных параметров:

> php bin/console debug:config security  Current configuration for extension with alias "security" =========================================================  security:     encoders:         App\Entity\User:             algorithm: bcrypt             migrate_from: {  }             hash_algorithm: sha512             key_length: 40             ignore_case: false             encode_as_base64: true             iterations: 5000             cost: null             memory_cost: null             time_cost: null             threads: null

Для тестов нам абсолютно не нужна такая криптоустойчивость, попробуем использовать md5 без каких-либо итераций:

# config/packages/test/security.yaml security:   encoders:     App\Entity\User:       algorithm: md5       encode_as_base64: false       iterations: 0

Проверяем:

Time: 02:49.090, Memory: 439.00 MB OK (1494 tests, 5536 assertions)

Hoooraaay! Давайте посмотрим сможем ли мы побежать еще быстрее…

Настраиваем логирование

В Doctrine ORM есть возможность логировать каждый выполняемый SQL запрос:

doctrine:     dbal:         logging: '%kernel.debug%'

Явно отключаем логирование для тестовой среды и изменяем уровень для остальных логов с debug на critical:

# config/packages/test/doctrine.yaml doctrine:     dbal:         logging: false  # config/packages/test/monolog.yaml monolog:     handlers:         docker:             type: stream             path: "php://stderr"             level: critical             channels: ["!event"]

Как результат, получаем ускорение в 20-30 секунд:

Time: 02:21.818, Memory: 445.00 MB OK (1494 tests, 5537 assertions)

ParaTest

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

> php-no-xdebug.sh vendor/bin/paratest  Running phpunit in 8 processes with /var/www/web/vendor/phpunit/phpunit/phpunit  Configuration read from /var/www/web/phpunit.xml.dist  ..............................  Time: 02:03.122, Memory: 12.00 MB  OK (1494 tests, 5537 assertions)

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

Заключение

Используя все эти оптимизации, играясь с настройками и библиотеками я достиг оптимального времени выполнения всех тестов, снизив его с 20 до 2 минут.

Если у вас есть советы как ещё можно ускориться, буду рад их услышать и опробовать.

ссылка на оригинал статьи https://habr.com/ru/post/505736/


Комментарии

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

Ваш адрес email не будет опубликован. Обязательные поля помечены *