14. Тестування. PHPUnit або Pest. Тестування Laravel Action. Test coverage.
Відео версія
(YouTube - для зворотнього зв'язку)
TL;DR
Набір порад по роботі з тестами. Деякі поради відносяться саме до Action підходу.
Підтримка - НАБАГАТО дорожча, ніж розробка. Розробка з тестами - це +20% до часу. Підтримка без тестів - це кіт у мішку.
Пишіть тести. Навіть якщо робите MVP. Якщо ви найнятий розробник - MVP переписати, скоріше за все, не дадуть. Зате змусять підтримувати той ASAP код аж до смерті проєкту, або ж до звільнення. А це зазвичай основний core код.
Pest vs PHPUnit - це питання синтаксису і все. Не важливо, що обрати для проєкту. Але, будь ласка, використовуйте щось одне. Розробник не повинен кожен раз робити вибір.
Тести - це також код. Їх також треба підтримувати. Тому правила code style повинні підтримуватися. І стандарти code complexity також!
Test coverage - не ідеальний показник, але він ігрофікує розробку - раджу використовувати.
Feature
в Laravel - це інтеграційні тести - всі тести, що тестують кінцевий метод.$this->get()
,$this->post()
,$this->getJson()
,$this->postJson()
...
Unit test - тест конкретного класу: action, filter, controller, form і так далі.
Ніколи не тестуйте приватні методи. Навіть якщо знайшли бібліотеки для цього.
Ніколи не використовуйте in-memory database (sqlite). Завжди тестуйте в тих DB, з якими працюєте, з тими ж версіями!
Для Action не бійтесь додавати декоратори, якщо це спростить тестування. Наприклад, для складних AsController - тестуйте доступ до методу в feature тестах, далі додайте AsObject (навіть якщо не використовується як об'єкт) і протестуйте реалізацію в unit тесті.
AsFake - дуже корисний декоратор. Mock Action зробить тестування Action, що використовують інші Action, дуже комфортним. Але це працює у випадку, якщо кожна Action протестована. І взагалі mock дуже корисна штука.
"Один тест - одна умова" - просте правило, яке дозволить зробити ваші тести дійсно лаконічними.
Тестуйте один функціонал за раз. Не треба викликати метод збереження даних перед тим, як їх отримувати. Використовуйте фабрики та mocks.
Тести не заміняють Manual QA.
Test DB in docker
Не використовуйте in-memory
DB. Це або обмежить вам можливості використовувати особливості основної БД, або змусить писати безліч специфічних умов.
У докері створити базу даних для тестів займе 5 хвилин.
db:
image: &db-image postgis/postgis:16-3.4-alpine
restart: unless-stopped
container_name: bh-db
environment:
POSTGRES_DB: ${DB_DATABASE}
POSTGRES_USER: ${DB_USERNAME}
POSTGRES_PASSWORD: ${DB_PASSWORD}
ports:
- 5432:5432
volumes:
- db_data:/var/lib/postgresql/data
networks:
- bh-db-network
healthcheck:
test: pg_isready -U ${DB_USERNAME} -d ${DB_DATABASE}
interval: 3s
timeout: 10s
retries: 3
db-test:
image: *db-image
container_name: bh-db-test
command: postgres -c 'max_connections=250' -c 'max_locks_per_transaction=128'
restart: unless-stopped
environment:
POSTGRES_DB: test_bh_db
POSTGRES_USER: test_bh_user
POSTGRES_PASSWORD: test_bh_user_password
networks:
- bh-db-network
&db-image
- посилання на image версію основної бази даних.*db-image
- використання тієї ж версії, що і за посиланням.POSTGRES_DB
,POSTGRES_USER
,POSTGRES_PASSWORD
- можна хардкодити, ваші тестові дані навряд чи кому цікаві, і вони ізольовані в контейнері.command: postgres -c 'max_connections=250' -c 'max_locks_per_transaction=128'
- розширюємо кількість одночасних підключень до Postgres БД та кількість транзакцій. Важливо для комфортного запуску паралельних тестів.
Laravel config
<source>
<include>
<directory>./Modules/</directory>
</include>
<exclude>
<directory>./Modules/**/Routes</directory>
<directory>./Modules/**/Tests</directory>
<directory>./Modules/**/Database</directory>
</exclude>
</source>
<php>
<env name="LOG_CHANNEL" value="stderr"/>
<env name="DB_CONNECTION" value="pgsql"/>
<env name="DB_HOST" value="bh-db-test"/>
<env name="DB_DATABASE" value="test_bh_db"/>
<env name="DB_PORT" value="5432"/>
<env name="DB_USERNAME" value="test_bh_user"/>
<env name="DB_PASSWORD" value="test_bh_user_password"/>
</php>
- У секції
php
налаштування БД таLOG_CHANNEL
- тести пишуть у ваші log за замовчуванням. - У секції
source
- директорії для сканування test coverage. Додавайте та виключайте директорії звідти - в залежності від структури проєкту.
use RefreshDatabase;
private bool $dropTypes = true;
RefreshDatabase
- кожен тест проходить у транзакції, яка скасовується в кінці тесту. Найпродуктивніший спосіб ізолювати тести один від одного.dropTypes
- видаляє кастомні типи даних при міграції, для Postgres обов'язково.- Якщо використовуєте view, встановіть
dropViews
до true.
Test coverage setup
Test coverage - важка операція і вимагає багато часу. Не треба її встановлювати на CI/CD. Але локально слід дозволити перевірку. Вона допоможе розробнику, нагадавши, що щось не протестовано.
XDebug.
Swoole не працює з Xdebug. Дивним чином падає та конфліктує !
COPY php/xdebug.ini /usr/local/etc/php/conf.d
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS linux-headers autoconf linux-headers && \
pecl install -o -f xdebug && \
apk del .build-deps
В ini активуйте Xdebug з coverage mode
xdebug.mode=coverage
Pcov
COPY php/pcov.ini /usr/local/etc/php/conf.d
RUN apk add --no-cache --virtual .build-deps $PHPIZE_DEPS linux-headers && \
pecl install pcov && docker-php-ext-enable pcov && \
apk del .build-deps
В ini активуйте pcov та вкажіть root directory
pcov.enabled = 1
pcov.directory = /var/www
CI/CD
Раджу встановити запуск тестів як команду composer. Це дасть змогу додати команду до запуску тестів.
{
"scripts": {
"test": [
"@php artisan config:clear",
"@php artisan test --parallel --stop-on-failure --stop-on-error"
]
}
}
Також - запускайте тести на pre-push
git hook. Це зменшить навантаження на build сервер. А насправді це комфорт розробників, які будуть знати, що їхні проколи не бачить лідер :)
docker exec -t bh-app npx validate-branch-name
docker exec -t bh-app composer test
І обов'язково як CI/CD степ.
Тут популярний варіант розбити CI/CD на різні етапи. Перший етап - встановлення залежностей. На другому етапі - паралельно запускаємо тести та static analysis в різних jobs. Це звучить ок, але на практиці composer залежностей зазвичай до 100 MB. А Docker контейнер понад 100 MB. І "економлячи" час на завантаженні залежностей, ми як мінімум розгортаємо Docker 3 рази, і контейнери в ньому мінімум 2 рази (передати як кеш або артефакт їх не вдасться). Тому рекомендую проганяти і тести, і статичний аналіз в одній job. Це не так красиво, але набагато продуктивніше.
stages:
- test
default:
image: docker:26.1.3-alpine3.19
services:
- docker:26.1.3-dind
test:
stage: test
before_script:
- cp .env.example .env
- echo UID=$(id -u) >> .env
- echo GID=$(id -g) >> .env
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker compose up app db-test -d --no-deps
- docker-compose exec app composer config -- gitlab-token.gitlab.com gitlab-ci-token "${CI_JOB_TOKEN}"
- docker compose exec app composer install --no-scripts
- docker compose exec app php artisan key:generate
script:
- docker compose exec app php artisan insights --no-interaction
- docker compose exec app vendor/bin/phpstan
- docker compose exec app php artisan migrate --seed --env=test
- docker compose exec app php artisan migrate:refresh --env=test
- docker compose exec app composer test
Тут зверніть увагу на migration команди. Цього ви не знайдете в рекомендаціях, але особисто я раджу. Така перевірка одразу покаже міграції зі зламаним down методом (якщо використовуєте). І перевірить seed.
Одна проблема - запускати треба в окремому .env файлі з мінімальною конфігурацією (підключення до БД). Обов'язково не називайте його testing
- бо доведеться налаштовувати всі конфіги (для тестів власне).
APP_NAME=Laravel
APP_ENV=test
APP_KEY=
APP_DEBUG=true
APP_URL=http://localhost
DB_CONNECTION=pgsql
DB_HOST=bh-db-test
DB_PORT=5432
DB_DATABASE=test_bh_db
DB_USERNAME=test_bh_user
DB_PASSWORD=test_bh_user_password