CI/CD Testing Pipelines
Continuous Integration and Continuous Deployment (CI/CD) pipelines automate your testing process, ensuring that every code change is thoroughly tested before reaching production. This lesson covers setting up automated testing pipelines using popular CI/CD platforms.
Understanding CI/CD Testing
CI/CD testing automatically runs your test suite whenever code changes are pushed to your repository. This provides immediate feedback and prevents bugs from reaching production.
Key Benefits:
- Immediate feedback on code changes
- Automated test execution on every commit
- Parallel test execution for faster results
- Test history and trend analysis
- Integration with deployment pipelines
GitHub Actions for Laravel Testing
GitHub Actions is a powerful CI/CD platform integrated directly into GitHub repositories. Here's a comprehensive Laravel testing workflow:
# .github/workflows/tests.yml
name: Laravel Tests
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
jobs:
laravel-tests:
runs-on: ubuntu-latest
strategy:
matrix:
php-version: [8.1, 8.2, 8.3]
laravel-version: [10.*, 11.*]
services:
mysql:
image: mysql:8.0
env:
MYSQL_ROOT_PASSWORD: password
MYSQL_DATABASE: testing
ports:
- 3306:3306
options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=3
redis:
image: redis:7
ports:
- 6379:6379
options: --health-cmd="redis-cli ping" --health-interval=10s --health-timeout=5s --health-retries=3
steps:
- uses: actions/checkout@v4
- name: Setup PHP
uses: shivammathur/setup-php@v2
with:
php-version: ${{ matrix.php-version }}
extensions: mbstring, dom, fileinfo, mysql, redis
coverage: xdebug
- name: Cache Composer dependencies
uses: actions/cache@v3
with:
path: vendor
key: composer-${{ matrix.php-version }}-${{ matrix.laravel-version }}-${{ hashFiles('**/composer.lock') }}
restore-keys: |
composer-${{ matrix.php-version }}-${{ matrix.laravel-version }}-
- name: Install Dependencies
run: |
composer require "laravel/framework:${{ matrix.laravel-version }}" --no-interaction --no-update
composer install --prefer-dist --no-interaction --no-progress
- name: Copy Environment File
run: cp .env.example .env
- name: Generate Application Key
run: php artisan key:generate
- name: Directory Permissions
run: chmod -R 777 storage bootstrap/cache
- name: Run Database Migrations
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
run: php artisan migrate --force
- name: Execute Unit Tests
env:
DB_CONNECTION: mysql
DB_HOST: 127.0.0.1
DB_PORT: 3306
DB_DATABASE: testing
DB_USERNAME: root
DB_PASSWORD: password
REDIS_HOST: 127.0.0.1
REDIS_PORT: 6379
run: php artisan test --parallel --coverage --min=80
- name: Upload Coverage Reports
uses: codecov/codecov-action@v3
with:
files: ./coverage.xml
flags: unittests
name: codecov-umbrella
GitLab CI Configuration
GitLab CI/CD provides powerful features for testing Laravel applications with its built-in container registry and deployment options:
# .gitlab-ci.yml
image: php:8.2-fpm
variables:
MYSQL_ROOT_PASSWORD: secret
MYSQL_DATABASE: testing
MYSQL_USER: laravel
MYSQL_PASSWORD: laravel
DB_HOST: mysql
stages:
- build
- test
- deploy
cache:
key: ${CI_COMMIT_REF_SLUG}
paths:
- vendor/
- node_modules/
before_script:
- apt-get update -yqq
- apt-get install -yqq git libzip-dev libpq-dev libcurl4-gnutls-dev
- docker-php-ext-install pdo_mysql zip
- curl -sS https://getcomposer.org/installer | php
- php composer.phar install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
- cp .env.example .env
- php artisan key:generate
- php artisan config:clear
build:
stage: build
script:
- composer validate
- composer install --prefer-dist --no-ansi --no-interaction --no-progress --no-scripts
- npm install
- npm run build
artifacts:
paths:
- vendor/
- node_modules/
- public/build/
expire_in: 1 hour
unit-tests:
stage: test
services:
- mysql:8.0
dependencies:
- build
script:
- php artisan migrate --seed
- php artisan test --testsuite=Unit --coverage-cobertura=coverage.xml
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: coverage.xml
coverage: '/^\s*Lines:\s*\d+.\d+\%/'
feature-tests:
stage: test
services:
- mysql:8.0
- redis:latest
dependencies:
- build
script:
- php artisan migrate --seed
- php artisan test --testsuite=Feature
retry:
max: 2
when: runner_system_failure
browser-tests:
stage: test
services:
- mysql:8.0
- selenium/standalone-chrome:latest
dependencies:
- build
script:
- php artisan dusk:chrome-driver --detect
- php artisan migrate --seed
- php artisan serve > /dev/null 2>&1 &
- sleep 5
- php artisan dusk
artifacts:
when: on_failure
paths:
- tests/Browser/screenshots/
- tests/Browser/console/
expire_in: 7 days
Parallel Test Execution
Running tests in parallel dramatically reduces pipeline execution time:
# GitHub Actions with Parallel Testing
jobs:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
test-suite: [Unit, Feature, Browser]
steps:
- uses: actions/checkout@v4
- name: Run ${{ matrix.test-suite }} Tests
run: php artisan test --testsuite=${{ matrix.test-suite }} --parallel
# PHPUnit configuration for parallel execution
<!-- phpunit.xml -->
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="./vendor/phpunit/phpunit/phpunit.xsd"
bootstrap="vendor/autoload.php"
colors="true"
processIsolation="false"
stopOnFailure="false">
<testsuites>
<testsuite name="Unit">
<directory suffix="Test.php">./tests/Unit</directory>
</testsuite>
<testsuite name="Feature">
<directory suffix="Test.php">./tests/Feature</directory>
</testsuite>
</testsuites>
<php>
<env name="PARALLEL_TESTING" value="true"/>
<env name="PARATEST_PROCESSES" value="4"/>
</php>
</phpunit>
Test Reporting and Artifacts
Comprehensive test reporting helps track test results over time:
// Generate HTML test report
php artisan test --coverage-html=coverage-report
// Generate coverage badge
php artisan test --coverage-clover=coverage.xml
// JUnit XML report for CI systems
php artisan test --log-junit=junit.xml
Test Report Best Practices:
- Store test reports as artifacts for 30-90 days
- Generate coverage badges for README files
- Track test execution time trends
- Set up notifications for test failures
- Archive failed test screenshots and logs
Environment-Specific Testing
Configure different test environments for various scenarios:
# .github/workflows/tests.yml
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
environment: [staging, production]
steps:
- name: Run tests against ${{ matrix.environment }}
env:
APP_ENV: ${{ matrix.environment }}
APP_URL: ${{ secrets[format('{0}_APP_URL', matrix.environment)] }}
DB_DATABASE: ${{ secrets[format('{0}_DB_DATABASE', matrix.environment)] }}
run: php artisan test
Database Seeding in CI
Efficiently seed test databases in your pipeline:
// Use dedicated CI seeder
class CISeed extends Seeder
{
public function run()
{
// Only create essential test data
User::factory()->count(10)->create();
Product::factory()->count(50)->create();
// Skip time-consuming operations
if (!app()->environment('ci')) {
$this->call(HeavyDataSeeder::class);
}
}
}
# In CI workflow
- name: Seed Database
run: php artisan db:seed --class=CISeed
Caching Strategies
Optimize pipeline speed with intelligent caching:
# GitHub Actions caching
- name: Cache Composer packages
uses: actions/cache@v3
with:
path: vendor
key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }}
restore-keys: |
${{ runner.os }}-composer-
- name: Cache NPM packages
uses: actions/cache@v3
with:
path: node_modules
key: ${{ runner.os }}-npm-${{ hashFiles('**/package-lock.json') }}
restore-keys: |
${{ runner.os }}-npm-
- name: Cache Laravel Routes
uses: actions/cache@v3
with:
path: bootstrap/cache
key: ${{ runner.os }}-routes-${{ hashFiles('routes/*.php') }}
Notifications and Integrations
Set up notifications for test results:
# Slack notification on failure
- name: Notify Slack on Failure
if: failure()
uses: 8398a7/action-slack@v3
with:
status: ${{ job.status }}
text: 'Tests failed on ${{ github.ref }}'
webhook_url: ${{ secrets.SLACK_WEBHOOK }}
- name: Create GitHub Issue on Failure
if: failure()
uses: actions/github-script@v6
with:
script: |
github.rest.issues.create({
owner: context.repo.owner,
repo: context.repo.repo,
title: 'CI Tests Failed: ${{ github.sha }}',
body: 'Tests failed in workflow run: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}'
})
Security Scanning in Pipelines
Integrate security checks into your test pipeline:
# Security vulnerability scanning
- name: Security Audit
run: |
composer audit
npm audit
- name: Static Analysis
run: |
vendor/bin/phpstan analyse --memory-limit=2G
vendor/bin/psalm --show-info=true
- name: Code Quality Check
run: vendor/bin/php-cs-fixer fix --dry-run --diff
Common Pipeline Pitfalls:
- Not caching dependencies (slow builds)
- Running all tests sequentially (wasted time)
- Ignoring flaky tests (unreliable results)
- Missing environment variables (test failures)
- Not cleaning up resources (memory leaks)
Deployment Gates
Use test results to control deployments:
# Deploy only if tests pass
deploy:
stage: deploy
needs:
- unit-tests
- feature-tests
- browser-tests
only:
- main
script:
- echo "Deploying to production..."
- ./deploy.sh
Exercise:
- Create a GitHub Actions workflow that runs your Laravel tests on push
- Add parallel test execution for Unit and Feature tests
- Configure MySQL and Redis services
- Set up code coverage reporting
- Add a caching strategy for Composer dependencies
- Bonus: Add Slack notifications for test failures
Monitoring Pipeline Performance
Track and optimize your pipeline execution time:
// Generate pipeline performance report
php artisan test --profile --testdox
// Output:
// Time: 00:02.548, Memory: 24.00 MB
//
// Slowest tests:
// - ProductTest::it_can_process_large_orders (1.2s)
// - UserTest::it_can_upload_profile_image (0.8s)
// - OrderTest::it_can_generate_invoice_pdf (0.6s)
Pipeline Optimization Checklist:
- Cache dependencies aggressively
- Use parallel execution for test suites
- Run fast tests first (fail fast strategy)
- Use database transactions instead of migrations
- Mock external API calls
- Use in-memory databases when possible
- Clean up test data efficiently
Best Practices Summary
- Run tests on every commit and pull request
- Use matrix builds to test multiple PHP/Laravel versions
- Implement parallel testing to reduce execution time
- Cache dependencies and build artifacts
- Generate and archive test reports
- Set up meaningful notifications
- Use deployment gates to prevent buggy releases
- Monitor and optimize pipeline performance
- Include security scans in your pipeline
- Document your CI/CD configuration
A well-configured CI/CD pipeline provides confidence that your code works correctly before it reaches production, enabling faster and safer deployments.