Test Coverage و CI در Laravel — PHPUnit، Pest و GitHub Actions
Xdebug coverage، حداقل درصد معقول، گزارش در CI و pipeline تست خودکار با GitHub Actions.
تست بدون CI روی لپتاپ developer میماند. Coverage هم عدد راهنماست نه هدف — اما در PR نشان میدهد کدام بخش بدون تست merge شده. این مقاله pipeline عملی Laravel + Pest/PHPUnit + GitHub Actions را میچیند.
خلاصه: «تستها پاس شدند» یعنی رفتارهایی که تست کردهاید درست کار میکنند؛ «coverage» فقط نشان میدهد هنگام اجرای تستها کدام خطوط کد اجرا شدهاند. برای coverage یک driver مثل Xdebug یا PCOV لازم است. در CI بهتر است شکست تست و شکست حد coverage را جدا ببینید، خروجی خطا را نگه دارید و مطمئن شوید تستها هرگز به دیتابیس واقعی وصل نمیشوند.
پاس شدن تست در برابر coverage
- تستی که هیچ assertion معناداری ندارد هم میتواند coverage را بالا ببرد؛ اجرا شدن یک خط به معنی درست بودن آن نیست.
- coverage بالا تضمین نمیکند حالتهای مرزی، خطاها و دسترسیها تست شده باشند.
- coverage پایین در یک بخش، نقشهٔ جاهایی است که هیچ تستی از آنها عبور نمیکند — مفیدترین کاربرد گزارش همین است.
اجرای coverage محلی
# PCOV سریعتر از Xdebug برای coverage
php -d pcov.enabled=1 ./vendor/bin/pest --coverage
# حداقل درصد
php -d pcov.enabled=1 ./vendor/bin/pest --coverage --min=70
نصب PCOV یا Xdebug در php.ini محیط dev.
دستورهای بالا فقط وقتی کار میکنند که Pest در پروژه نصب باشد (عدد 70 هم فقط مثال است). اگر پروژه فقط PHPUnit دارد، معادلش این است:
# گزارش متنی در ترمینال از طریق artisan
php artisan test --coverage
# مستقیم با PHPUnit و خروجی برای ابزارهای دیگر
./vendor/bin/phpunit --coverage-text
./vendor/bin/phpunit --coverage-html coverage-html
./vendor/bin/phpunit --coverage-clover coverage.xml
پیشنیاز driver
- PCOV: فقط برای coverage ساخته شده؛ باید extension نصب و فعال باشد.
- Xdebug 3: باید حالت coverage فعال باشد، مثلاً با
XDEBUG_MODE=coverageیاxdebug.mode=coverageدر php.ini.
اگر driver نباشد، php artisan test --coverage با پیام Code coverage driver not available متوقف میشود و PHPUnit هم هشدار «No code coverage driver available» میدهد. برای اطمینان، php -m را اجرا کنید و دنبال pcov یا xdebug بگردید.
خواندن گزارش
در گزارش متنی برای هر کلاس درصد خطوط (Lines)، متدها (Methods) و کلاسها (Classes) آمده است. بهجای عدد کل، دنبال اینها باشید: کلاسهای مهم کسبوکار با coverage صفر، متدهایی که فقط مسیر موفقشان اجرا شده و بخشهایی مثل پرداخت یا دسترسی که هیچ تستی ندارند. گزارش HTML خطوط اجرانشده را رنگی نشان میدهد و برای این بررسی راحتتر است.
phpunit.xml تنظیم coverage
<source>
<include>
<directory>app</directory>
</include>
<exclude>
<directory>app/Providers</directory>
</exclude>
</source>
Provider و route list معمولاً exclude — تمرکز روی domain.
چند درصد کافی است؟
- عدد جهانی درستی وجود ندارد؛ حد مناسب به ریسک کد، سن پروژه و نوع تستها بستگی دارد.
- برای پروژهٔ قدیمی بدون تست، حد بالا از روز اول فقط pipeline را قرمز نگه میدارد. اول coverage فعلی را اندازه بگیرید و حد را کمی پایینتر از آن بگذارید تا فقط از افت جلوگیری کند.
- بخشهای پرریسک (پرداخت، دسترسی، محاسبات مالی) را با تست هدفمند پوشش دهید، نه با بالا بردن عدد کل.
- ۱۰۰٪ هدف معمولاً waste — کیفیت تست مهمتر
اگر حد coverage باعث شود تیم تستهای بیassertion بنویسد تا عدد بالا برود، آن حد به کیفیت ضرر زده است. میتوانید مدتی coverage را فقط گزارش کنید و بدون حد اجباری پیش بروید.
GitHub Actions — workflow پایه
name: Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: '8.3'
extensions: mbstring, pdo_mysql
coverage: pcov
- name: Install dependencies
run: composer install --no-interaction --prefer-dist
- name: Copy env
run: cp .env.example .env
- name: Generate key
run: php artisan key:generate
- name: Run tests
run: php artisan test --parallel --coverage --min=70
php artisan test همان Pest/PHPUnit — Laravel 12.
این workflow یک نقطهٔ شروع است، نه نسخهٔ آماده برای هر پروژه. قبل از استفاده اینها را با پروژهٔ خودتان تطبیق دهید:
- نسخهٔ PHP:
php-versionرا با محدودهٔrequire.phpدر composer.json و نسخهٔ production یکی کنید. - extensionها: هر extensionی که پروژه لازم دارد (مثلاً gd، intl یا pdo_sqlite) را در لیست بیاورید.
--parallel: فقط با پکیجbrianium/paratestدر require-dev کار میکند؛ بدون آن artisan خطای پیشنیاز ParaTest میدهد.- driver coverage: مقدار
coverage: pcovدر setup-php، PCOV را در runner نصب میکند؛ اگر coverage لازم ندارید،coverage: noneبگذارید. - Vite: اگر viewها از
@viteاستفاده میکنند و manifest بیلدشده در repo نیست، یا در CInpm ciوnpm run buildاجرا کنید یا در تستها$this->withoutVite()بزنید.
جدا کردن شکست تست از شکست coverage
در php artisan test --coverage --min=… حد coverage فقط وقتی بررسی میشود که همهٔ تستها پاس شده باشند. پس دو نوع شکست، خروجی متفاوت دارند:
- شکست تست: نام تستهای FAILED، پیام assertion و stack trace در خروجی میآید. یعنی رفتار کد خراب است.
- شکست coverage: همهٔ تستها پاس شدهاند و آخر خروجی پیام
Code coverage below expectedهمراه درصد فعلی و حداقل آمده. یعنی کد درست کار میکند ولی تستها بخش کمتری از کد را اجرا میکنند.
برای خواناتر شدن pipeline میتوانید دو step جدا بگذارید: یک step برای اجرای تستها با گزارش JUnit و step بعدی برای گزارش یا حد coverage. هزینهاش این است که تستها دو بار اجرا میشوند؛ اگر زمان CI برایتان مهم است، یک step با --min و خواندن دقیق پیام آخر کافی است.
- name: Run tests
run: ./vendor/bin/phpunit --log-junit junit.xml
- name: Coverage report
run: ./vendor/bin/phpunit --coverage-clover coverage.xml --coverage-text
- name: Upload logs on failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-logs
path: |
junit.xml
storage/logs/*.log
step آخر با if: failure() فقط وقتی اجرا میشود که stepهای قبلی شکست خورده باشند و فایلهای لاگ را برای بررسی نگه میدارد.
MySQL در CI
services:
mysql:
image: mysql:8.0
env:
MYSQL_DATABASE: testing
MYSQL_ROOT_PASSWORD: password
ports:
- 3306:3306
.env.testing با DB testing — یا SQLite :memory: برای سرعت.
ایمنی env و دیتابیس تست
- متغیرهای
<env>در phpunit.xml (مثلAPP_ENV=testingوDB_DATABASE) را صریح بنویسید تا تستها هیچوقت به دیتابیس .env اصلی وصل نشوند. تستهایی کهRefreshDatabaseدارند جداول را پاک و دوباره میسازند. - اگر روی سیستم خودتان
php artisan config:cacheزدهاید، config کششده میتواند env تست را نادیده بگیرد؛ قبل از تستphp artisan config:clearبزنید. - در CI هرگز اطلاعات اتصال دیتابیس یا سرویسهای production را قرار ندهید؛ secretهای لازم را از GitHub Secrets بخوانید و echo نکنید.
برای نحوهٔ کار env و فایلهای .env.*، راهنمای تنظیمات env در Laravel را ببینید.
گزارش coverage در PR
Actionهای third-party مثل codecov یا coveralls — upload بعد از test:
- name: Upload coverage
uses: codecov/codecov-action@v4
with:
files: ./coverage.xml
Pest: --coverage-clover coverage.xml
در PHPUnit هم همین گزینه وجود دارد: ./vendor/bin/phpunit --coverage-clover coverage.xml. نسخهٔ action و نیاز به token را از مستندات همان سرویس بررسی کنید.
Pest parallel در CI
php artisan test --parallel --recreate-databases
سریعتر — worker جدا. Pest.
اجرای موازی به brianium/paratest نیاز دارد و تستهایی را که به ترتیب اجرا یا state مشترک وابستهاند آشکار میکند. اگر بعد از فعال کردن --parallel تستی تصادفی شکست خورد، احتمالاً مشکل از وابستگی بین تستهاست، نه از CI.
شکست pipeline = شکست merge
branch protection: require status check «Tests» قبل از merge.
چه تستی در CI؟
- Unit + Feature همه (برای تست درخواستهای HTTP، Feature test در Laravel)
- بدون test skipped بدون دلیل
- lint اختیاری: Pint در job جدا
اشتباهات
- coverage ۱۰۰٪ اجباری — تست بیمعنی
- بدون .env.testing — flaky DB
- secret در workflow log
- فرض اینکه driver coverage در runner نصب است؛ بدون آن، step coverage شکست میخورد در حالی که تستها سالماند.
- حد coverage بدون اندازهگیری وضعیت فعلی پروژه.
تست محلی سبز، CI قرمز
وقتی تستها روی سیستم شما پاس میشوند ولی در CI شکست میخورند، معمولاً یکی از این تفاوتهاست:
- config کششده یا .env محلی: تست محلی از تنظیماتی استفاده میکند که در CI وجود ندارد.
- حساسیت به حروف بزرگ و کوچک: ویندوز و macOS معمولاً نام فایل را بدون حساسیت به حروف میخوانند، ولی runner لینوکسی حساس است؛
Models/post.phpدر برابرPostدر CI پیدا نمیشود. - extension یا نسخهٔ PHP متفاوت: extensionی که روی XAMPP فعال است در runner نصب نشده.
- Vite manifest: فایلهای build روی سیستم شما هست ولی در CI نه.
- ترتیب تستها: تستی که به داده یا state تست قبلی وابسته است، با ترتیب یا اجرای موازی متفاوت شکست میخورد.
- دیتابیس متفاوت: تست محلی روی SQLite و CI روی MySQL (یا برعکس)؛ تفاوت در collation، نوع ستون یا رفتار foreign key.
اولین قدم همیشه خواندن کامل پیام خطا در لاگ CI است؛ برای بازتولید، همان دستور CI را با همان env محلی اجرا کنید.
جمعبندی
CI تضمین regression؛ coverage نقشه blind spot. Mock: Mockery. Doubles: Fakes. Deploy بعدی در DevOps cluster.
ترتیب پیشنهادی: اول تستها را در CI سبز و پایدار کنید، بعد driver coverage را اضافه کنید و گزارش را بخوانید، و فقط وقتی تصویر روشنی از وضعیت فعلی داشتید دربارهٔ حد coverage تصمیم بگیرید.