Kokil Thapa - Professional Web Developer in Nepal
Freelancer Web Developer in Nepal with 15+ Years of Experience

Kokil Thapa is an experienced full-stack web developer focused on building fast, secure, and scalable web applications. He helps businesses and individuals create SEO-friendly, user-focused digital platforms designed for long-term growth.

Laravel Testing with Pest in CI/CD

By Kokil Thapa | Last reviewed: September 2026

Laravel Testing with Pest in CI/CD is how you stop broken code from reaching production on a real client project. Pest gives you expressive tests that read like specs, and your pipeline runs them on every push before a deploy is allowed. If you already ship Laravel with GitLab CI pipelines for Laravel, adding Pest is a small change with a big payoff. This guide covers install, pipeline config, caching, coverage gates, and the failures I see most often in 2026.

What is Laravel Testing with Pest in CI/CD and why does it matter?

Pest is a testing framework built on PHPUnit. It wraps Laravel's test runner with a cleaner syntax. CI/CD is the automation layer that runs those tests before merge or deploy. Together they form a safety net you can trust.

On production Laravel applications I maintain, the pattern is simple. A developer pushes code. The pipeline installs dependencies, prepares a test database, runs Pest, and blocks the merge if anything fails. Deploy only happens after green tests. That is not optional for booking systems, payment flows, or legal-tech portals where a regression costs real money.

Pest works with Laravel 12 and Laravel 13. Laravel 13 requires PHP 8.3 or higher. Laravel 12 runs on PHP 8.2+. Most CI images ship PHP 8.4 or 8.5 today, which is fine. Match your CI PHP version to production. A version mismatch is a common source of false greens or false reds.

Laravel Testing with Pest in CI/CDGit Pushfeature branchCI Jobcomposer installPest Suitephp artisan testDeployonly if greenTest Layers in a Typical Laravel PipelineUnitpure logicFeatureHTTP + DBAPIJSON contractsAny failure stops deploy — no exceptions
Laravel Testing with Pest in CI/CD: push triggers install, Pest runs, deploy waits for a green suite.

If you are still on PHPUnit syntax, read the Pest migration from PHPUnit guide first. The CI wiring stays almost identical because Pest delegates to PHPUnit under the hood.

How do you install Pest and write your first CI-ready Laravel tests?

Start in a Laravel 12 or 13 project on PHP 8.3+. Install Pest with Composer 2.10:

composer require pestphp/pest --dev --with-all-dependencies
composer require pestphp/pest-plugin-laravel --dev
php artisan pest:install

The installer creates tests/Pest.php and updates phpunit.xml. Your CI job will call the same entry point Laravel ships:

php artisan test

That command works for both Pest and PHPUnit tests during a gradual migration. A minimal feature test for a booking route might look like this:

<?php

use App\Models\User;

it('redirects guests away from the dashboard', function () {
    $this->get('/dashboard')
        ->assertRedirect('/login');
});

it('shows the dashboard for authenticated users', function () {
    $user = User::factory()->create();

    $this->actingAs($user)
        ->get('/dashboard')
        ->assertOk()
        ->assertSee('Dashboard');
});

Configure phpunit.xml for CI

Your phpunit.xml must use environment variables the pipeline sets. Never hard-code production credentials. A CI-safe baseline:

<env name="APP_ENV" value="testing"/>
<env name="APP_KEY" value="base64:TESTKEYTESTKEYTESTKEYTESTKEYTESTKEYTEST="/>
<env name="DB_CONNECTION" value="mysql"/>
<env name="DB_HOST" value="127.0.0.1"/>
<env name="DB_DATABASE" value="laravel_test"/>
<env name="DB_USERNAME" value="root"/>
<env name="DB_PASSWORD" value="secret"/>
<env name="CACHE_STORE" value="array"/>
<env name="QUEUE_CONNECTION" value="sync"/>
<env name="SESSION_DRIVER" value="array"/>

Use RefreshDatabase on feature tests that touch the database. For heavier suites, consider the parallel test runs with ParaTest approach once your test count grows past a few hundred.

Local parity before CI

Run the same command locally that CI runs. Match PHP extensions too. If CI uses pdo_mysql and your laptop uses SQLite, you will chase environment-only failures for weeks.

  1. Install Pest and the Laravel plugin.
  2. Write at least one unit test and one feature test.
  3. Configure phpunit.xml with testing env vars.
  4. Run php artisan test locally until green.
  5. Add the same command to your CI job.

How do you configure GitLab CI for Laravel Pest tests?

GitLab CI is what I use on several sister sites sharing a Deployer 7 pipeline. The test stage belongs before build or deploy. Here is a production-ready .gitlab-ci.yml excerpt for Laravel 13 on PHP 8.4 with MySQL 8.4:

stages:
  - test
  - deploy

variables:
  MYSQL_ROOT_PASSWORD: secret
  MYSQL_DATABASE: laravel_test
  DB_CONNECTION: mysql
  DB_HOST: mysql
  DB_DATABASE: laravel_test
  DB_USERNAME: root
  DB_PASSWORD: secret

pest:
  stage: test
  image: php:8.4-cli
  services:
    - name: mysql:8.4
      alias: mysql
  cache:
    key: ${CI_COMMIT_REF_SLUG}-composer
    paths:
      - vendor/
  before_script:
    - apt-get update && apt-get install -y git unzip libzip-dev libpng-dev
    - docker-php-ext-install pdo_mysql zip gd
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
    - cp .env.testing .env
    - composer install --no-interaction --prefer-dist --no-progress
    - php artisan key:generate
    - php artisan migrate --force
  script:
    - php artisan test --parallel
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Cache vendor/ to speed up runs. See CI/CD caching for Composer and npm installs for key strategies that do not serve stale dependencies.

Keep secrets out of the YAML file. Use GitLab CI variables for anything sensitive. The CI/CD secrets management guide covers masked variables and rotation patterns I use on shared EC2 deploys.

Pest Execution Inside a CI Jobbefore_script: extensions, composer install, migratephp artisan test --parallelUnit testsfast, no DBFeature testsHTTP + MySQLAPIJSONExit 0 — deploy stage unlockedExit 1 — pipeline fails, no deploy
Inside a CI job, Pest runs after migrate; exit code decides whether deploy proceeds.

How do you run Pest tests in GitHub Actions for Laravel?

GitHub Actions fits teams already on GitHub. The workflow mirrors GitLab CI. Pin actions to stable versions and use service containers for MySQL:

name: Laravel Pest Tests

on:
  push:
    branches: [main]
  pull_request:

jobs:
  pest:
    runs-on: ubuntu-latest
    services:
      mysql:
        image: mysql:8.4
        env:
          MYSQL_ROOT_PASSWORD: secret
          MYSQL_DATABASE: laravel_test
        ports:
          - 3306:3306
        options: >-
          --health-cmd="mysqladmin ping -h 127.0.0.1"
          --health-interval=10s
          --health-timeout=5s
          --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - uses: shivammathur/setup-php@v2
        with:
          php-version: '8.4'
          extensions: mbstring, pdo_mysql, zip, gd
          coverage: xdebug
      - uses: actions/cache@v4
        with:
          path: vendor
          key: composer-${{ hashFiles('composer.lock') }}
      - run: composer install --no-interaction --prefer-dist
      - run: cp .env.testing .env
      - run: php artisan key:generate
      - run: php artisan migrate --force
        env:
          DB_HOST: 127.0.0.1
          DB_PASSWORD: secret
      - run: php artisan test --coverage --min=70

Read the full GitHub Actions for Laravel testing and deploy article for deploy job wiring. Split test and deploy into separate jobs. Deploy should need the test job via needs: pest.

Official references worth bookmarking: the Pest installation docs and the Laravel 13.x testing documentation.

Which CI/CD platform works best for Pest — GitLab CI or GitHub Actions?

Both run Pest equally well. The choice depends on where your repo lives and who maintains the runners. Here is a practical comparison for Laravel teams in 2026:

CriterionGitLab CIGitHub Actions
Best fitSelf-hosted GitLab, Deployer pipelines, private runnersGitHub-native repos, open source, marketplace actions
MySQL serviceservices: block with aliasservices: container with health check
Composer cacheBuilt-in cache: key per branchactions/cache keyed on lock file hash
Parallel Pestphp artisan test --parallelSame command, scale via matrix shards
SecretsCI/CD variables, maskedRepository secrets, environments
Deploy gaterules: + manual deploy stageneeds: + environment protection
Cost on small teamsFree tier minutes, self-hosted is cheap2,000 free minutes/month on private repos

For VPS deploys after tests pass, pair either platform with the GitLab CI/CD to VPS guide or the GitHub Actions equivalent. The test stage stays the same. Only the deploy job changes.

CI Platform Choice for PestGitLab CISelf-hosted + Deployer 7Shared runners on EC2MR pipelines built-inPick for GitLab reposGitHub ActionsMarketplace PHP actionsMatrix PHP version testsPR checks nativePick for GitHub reposBoth run php artisan test identically
GitLab CI and GitHub Actions both support Laravel Pest — choose based on repo host and ops setup.

How do you add coverage gates and catch flaky tests in CI?

Passing tests are the baseline. Coverage gates stop untested code from shipping silently. Pest supports coverage through PHPUnit's Xdebug or PCOV integration:

php artisan test --coverage --min=70

Set the minimum to what your team can sustain. Seventy percent is a reasonable starting point for a mature Laravel app. Read code coverage gates in CI before enforcing strict thresholds on legacy codebases.

Flaky test patterns I see in production

  • Time-dependent assertions — use Carbon::setTestNow() or freeze time in Pest's beforeEach.
  • Missing RefreshDatabase — tests pass alone but fail in suite order.
  • External HTTP calls — mock with Http::fake(); never hit live APIs in CI.
  • Filesystem assumptions — use Storage::fake('local') for upload tests.
  • Race conditions in parallel runs — isolate tests that share Redis or cache keys.

For API-heavy apps, combine Pest feature tests with the patterns in Laravel feature testing best practices and building RESTful APIs with Laravel. Validate JSON shape, status codes, and auth middleware in one test file per endpoint group.

Database restore testing catches migration drift. The database restore testing guide shows how to verify backups against a fresh schema — a step many teams skip until production breaks.

Top CI Failures and FixesPHP version mismatchCI 8.4 vs prod 8.3Missing pdo_mysqlextension not installedPin CI to prod PHPdocker-php-ext-installInstall extensionsin before_scriptMySQL not readymigrate runs too earlyStale vendor cachelock file changedAdd health check waitKey cache on lock hash
Most Laravel Pest CI failures come from PHP version drift, missing extensions, or database timing — all fixable in config.

On legal-tech portals and eCommerce systems, tests must cover more than happy paths. Document uploads, payment callbacks, and role-based access need dedicated Pest files.

Test payment webhooks without live gateways

it('marks an order paid when the webhook is valid', function () {
    Http::fake();
    $order = Order::factory()->create(['status' => 'pending']);

    $payload = ['order_id' => $order->id, 'status' => 'paid'];

    $this->postJson('/webhooks/khalti', $payload, [
        'X-Signature' => validSignature($payload),
    ])->assertOk();

    expect($order->fresh()->status)->toBe('paid');
});

Projects like Adventure Third Pole Trek and Mijar Law Associates depend on booking and payment flows that cannot break silently. Pest makes these scenarios readable for the next developer who touches the code.

Custom expectations for domain rules

Register reusable expectations in tests/Pest.php for Nepali date handling or VAT calculations. Your CI suite then enforces business rules on every push. For date logic, cross-check edge cases with the Nepali date converter tool during local test design.

If you need help wiring tests into an existing pipeline, the testing and optimization service covers audit, setup, and coverage improvement. For greenfield apps, start with enterprise application development where CI is planned from day one.

Key Takeaways

  • Install Pest with the Laravel plugin, then run php artisan test — the same command works locally and in CI.
  • Pin CI PHP to your production version and install pdo_mysql, zip, and gd before Composer runs.
  • Use a real MySQL service container in CI; SQLite-only local dev causes false greens.
  • Cache vendor/ keyed on composer.lock to keep pipeline times under five minutes.
  • Add --coverage --min=70 once your suite is stable; do not gate legacy code on day one.
  • Block deploy stages until the Pest job exits 0 — no manual override without a documented exception.

People Also Ask

Does Pest replace PHPUnit in Laravel?

Pest sits on top of PHPUnit. It changes syntax, not the underlying runner. Laravel's php artisan test command detects and runs Pest tests automatically. You can migrate file by file without breaking CI.

Can you run Pest tests in parallel in CI?

Yes. Use php artisan test --parallel after installing brianium/paratest. Parallel runs cut wall-clock time on large suites. Ensure tests do not share mutable global state or fixed cache keys.

What PHP version should CI use for Laravel 13?

Laravel 13 requires PHP 8.3 or higher. Pin CI to the same minor version as production. PHP 8.4 and 8.5 work in CI if production matches. Mismatched versions cause extension and deprecation surprises.

How long should a Laravel Pest CI job take?

A well-cached pipeline with under 300 tests should finish in three to eight minutes. If runs exceed fifteen minutes, add parallel execution, trim integration tests, or split jobs with a test matrix. Slow pipelines get ignored.

Ship with confidence

Laravel Testing with Pest in CI/CD turns your test suite from a local habit into a deploy gate. Install Pest, wire php artisan test into GitLab CI or GitHub Actions, cache Composer dependencies, and block deploys on failure. Start small with feature tests on critical paths — logins, bookings, payments — then expand coverage over time.

For teams without pipeline experience, read CI/CD best practices for small teams and GitLab CI/CD for PHP projects. Need hands-on help wiring Pest into your deploy flow? Contact us or explore the full web development service and recent Court Marriage in Nepal portfolio work built on the same GitLab CI + Deployer stack.

Frequently Asked Questions

Installing Pest in Laravel, running php artisan test on every push in your pipeline, failing on any test failure, and deploying only when the suite passes on PHP 8.3+ with a real database service.

In a Laravel 12 or 13 project on PHP 8.3+, run composer require pestphp/pest --dev --with-all-dependencies, then composer require pestphp/pest-plugin-laravel --dev, followed by php artisan pest:install. That creates tests/Pest.php and updates phpunit.xml. Write at least one unit and one feature test, configure phpunit.xml with testing environment variables, run php artisan test locally until green, then add the same command to your CI job. Use RefreshDatabase on feature tests that touch the database.

Add a test stage before deploy in .gitlab-ci.yml using a php:8.4-cli image and a mysql:8.4 service aliased as mysql. Cache vendor/ keyed per branch, install git, unzip, pdo_mysql, zip, and gd extensions, run composer install, copy .env.testing to .env, generate a key, migrate with --force, then run php artisan test --parallel. Set DB variables in the job and keep secrets in GitLab CI variables, not in the YAML file. Trigger on merge requests and the default branch.

Create a workflow with a mysql:8.4 service container, health checks, and port 3306 exposed. Use actions/checkout@v4, shivammathur/setup-php@v2 with PHP 8.4 and extensions mbstring, pdo_mysql, zip, gd, plus Xdebug for coverage. Cache vendor/ keyed on composer.lock, run composer install, copy .env.testing, generate a key, migrate with DB_HOST set to 127.0.0.1, then php artisan test --coverage --min=70. Split test and deploy into separate jobs and make deploy depend on the test job via needs.

No. Pest sits on top of PHPUnit and changes syntax, not the runner. php artisan test detects and runs Pest tests automatically, and you can migrate file by file without breaking CI.

Both run Pest equally well; the choice depends on where your repo lives and who maintains runners. GitLab CI fits self-hosted GitLab, Deployer pipelines, and private runners with built-in branch-keyed Composer caching. GitHub Actions suits GitHub-native repos with actions/cache keyed on composer.lock and environment protection on deploy. GitLab offers free tier minutes with cheap self-hosted runners; GitHub gives 2,000 free minutes monthly on private repos. The test stage stays identical on either platform.

Laravel 13 requires PHP 8.3 or higher. Pin your CI image to the same minor version as production — PHP 8.4 or 8.5 works if production matches. A version mismatch between CI and production is a common source of false greens or false reds caused by extension differences and deprecation behaviour.

Pest supports coverage through PHPUnit's Xdebug or PCOV integration. Run php artisan test --coverage --min=70 in your pipeline job. Set the minimum to what your team can sustain — seventy percent is a reasonable starting point for a mature Laravel app. Do not enforce strict thresholds on legacy codebases on day one. In GitHub Actions, enable coverage in setup-php with coverage: xdebug before running the test command.

If CI uses pdo_mysql with a real MySQL 8.4 service container but your laptop uses SQLite, you will chase environment-only failures for weeks. Configure phpunit.xml with DB_CONNECTION mysql and credentials matching your pipeline, then run migrate before tests. SQLite-only local development often produces false greens that break once real database constraints, timing, and MySQL-specific behaviour appear in CI.

A well-cached pipeline with under 300 tests should finish in three to eight minutes. Runs exceeding fifteen minutes need parallel execution, trimmed integration tests, or a test matrix split.

The article's most common causes are PHP version drift between laptop and pipeline, missing extensions like pdo_mysql, zip, or gd, and database timing issues before MySQL is ready. Hard-coded production credentials in phpunit.xml instead of CI environment variables also breaks jobs. Fix by matching PHP versions, installing the same extensions CI uses, adding service health checks, and running the identical php artisan test command locally before pushing.

Use php artisan test --parallel after installing brianium/paratest. Parallel runs cut wall-clock time on large suites once your test count grows past a few hundred. Ensure tests do not share mutable global state or fixed cache keys, which cause race conditions when jobs run concurrently. GitLab CI and GitHub Actions both support the same --parallel flag without platform-specific changes.

Freeze time with Carbon::setTestNow() instead of time-dependent assertions. Apply RefreshDatabase on tests that touch the database so suite order does not cause failures. Mock external HTTP with Http::fake() and never hit live APIs in CI. Use Storage::fake for upload tests. Isolate tests that share Redis or cache keys when running parallel jobs. Flaky pipelines get ignored by teams, so fixing these patterns keeps your deploy gate trustworthy.

Place the Pest job in a test stage that runs before build or deploy. The job must exit 0 for the pipeline to continue — any test failure blocks the merge or deploy. In GitLab CI, use stage ordering with rules on merge requests and the default branch. In GitHub Actions, split test and deploy into separate jobs and set deploy to need the pest job. Avoid manual deploy overrides unless you document a formal exception process.

Never hard-code production credentials. Set APP_ENV to testing, provide a test APP_KEY, point DB_CONNECTION to mysql with host, database, username, and password matching your pipeline variables, and use array drivers for CACHE_STORE and SESSION_DRIVER with QUEUE_CONNECTION set to sync. These values align with what GitLab CI or GitHub Actions inject via environment variables, keeping your test suite isolated from production data while running the same command locally and in CI.

Share this article

0 Comments

Leave a comment

Your email is not published. Comments appear once they have been read. Sign in to have your details filled in.

Quick Contact Options
Choose how you want to connect me: