Testing & TDD

CI/CD Testing Pipelines

20 min Lesson 26 of 35

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:
  1. Create a GitHub Actions workflow that runs your Laravel tests on push
  2. Add parallel test execution for Unit and Feature tests
  3. Configure MySQL and Redis services
  4. Set up code coverage reporting
  5. Add a caching strategy for Composer dependencies
  6. 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.