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.

Integration Testing in CI Pipelines

By Kokil Thapa | Last reviewed: September 2026

Your unit tests pass locally, then production breaks on a payment callback or a migration edge case. Integration Testing in CI Pipelines closes that gap by exercising real database connections, HTTP routes, queues, and third-party boundaries inside the same pipeline that builds your artefact. On production Laravel applications I maintain, integration tests sit between fast unit checks and slow browser suites. They run on every push through a GitLab CI pipeline for Laravel before Deployer swaps the release symlink. This guide shows exactly how to wire that stage for PHP 8.3+, Laravel 12 or 13, MySQL, and Redis in 2026.

What is Integration Testing in CI Pipelines and why does it matter?

Integration tests verify that modules work together with real infrastructure. Unit tests mock the database; integration tests hit it. In CI, that means spinning up service containers beside your app image and running PHPUnit or Pest against a disposable schema.

The payoff is practical. I've seen Laravel apps pass 400 unit tests, then fail in staging because a foreign key migration order was wrong. Integration tests in CI would have caught that in four minutes. They also validate database migrations in CI/CD pipelines against a fresh database every run—exactly what production does on first deploy.

CI Pipeline with Integration TestsCommitgit pushBuildcomposer installUnit Testsfast mocksIntegrationDB + Redis + HTTPMySQL 9.7service containerRedis 8.10queue + cacheMail Fakeno outbound SMTPDeploy Stageonly if integration passes
Integration Testing in CI Pipelines sits after build and unit tests, using real service containers before deploy.

Teams that skip this stage often rely on manual QA after merge. That works until traffic grows or payment webhooks multiply. For booking systems like Adventure Third Pole Trek, integration tests around reservation holds and supplier notifications saved repeated staging fire drills.

What belongs in the integration layer

  • HTTP feature tests that POST to routes and assert database state
  • Queue job dispatch with Redis and Queue::fake() only where isolation demands it
  • Migration runs against an empty schema on every pipeline execution
  • Payment gateway sandbox calls with credentials from CI variables
  • File upload flows using the local disk driver inside the job container

How do integration tests differ from unit and E2E tests in CI?

The test pyramid still applies in 2026. Unit tests stay fast and isolated. End-to-end browser tests are expensive and flaky in CI. Integration tests occupy the middle: slower than units, cheaper than Dusk or Playwright, and far more realistic than mocks alone.

Test typeCI runtime (typical Laravel app)DependenciesBest for
Unit30–90 secondsNone (mocked)Pure logic, validators, DTOs
Integration2–8 minutesMySQL, Redis, mail fakeRoutes, Eloquent, jobs, migrations
E2E / browser10–30+ minutesFull stack + ChromeCritical checkout or login flows

Laravel's official testing docs distinguish Unit and Feature test directories. In CI terminology, most Feature tests are integration tests when they use RefreshDatabase or hit external HTTP with Http::fake(). Pure unit tests never boot the full application kernel. See the Laravel 12 testing documentation for the framework's own taxonomy.

For deeper patterns on HTTP assertions, read Laravel feature testing best practices. For Pest syntax in pipelines, see Laravel testing with Pest in CI/CD.

CI Test PyramidUnit Testsmany, fastIntegrationCI sweet spotE2E Browserfew, nightlyRun every pushRun on schedule
Integration Testing in CI Pipelines targets the middle layer—real services without full browser overhead.

How do you configure GitLab CI for Laravel integration tests?

GitLab CI service containers share a Docker network with the job image. Your app connects to hostname mysql and redis, not 127.0.0.1. That single detail causes more pipeline failures than any assertion mismatch.

Below is a production-ready .gitlab-ci.yml fragment for Laravel 12 on PHP 8.3 with Composer 2.10. Adjust image tags to match your runner's available registry mirrors.

stages:
  - build
  - test
  - deploy

variables:
  MYSQL_ROOT_PASSWORD: root
  MYSQL_DATABASE: laravel_test
  DB_HOST: mysql
  DB_CONNECTION: mysql
  REDIS_HOST: redis
  APP_ENV: testing
  APP_KEY: base64:AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=

integration_tests:
  stage: test
  image: php:8.3-cli
  services:
    - name: mysql:8.4
      alias: mysql
    - name: redis:8
      alias: redis
  cache:
    key: ${CI_COMMIT_REF_SLUG}-composer
    paths:
      - vendor/
  before_script:
    - apt-get update -qq && apt-get install -y -qq git unzip libzip-dev libpng-dev
    - docker-php-ext-install pdo_mysql zip gd pcntl
    - pecl install redis && docker-php-ext-enable redis
    - curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer
    - composer install --no-interaction --prefer-dist --no-progress
    - cp .env.testing .env
    - php artisan key:generate --force
    - php artisan migrate --force --seed
  script:
    - php artisan test --parallel --testsuite=Feature
  artifacts:
    when: always
    reports:
      junit: storage/logs/junit.xml
    expire_in: 7 days
  rules:
    - if: $CI_PIPELINE_SOURCE == "merge_request_event"
    - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH

Enable JUnit output in phpunit.xml so GitLab surfaces failing test names in the merge request UI. The GitLab CI services documentation explains alias networking and health-check timing.

Wait for database readiness

MySQL may accept TCP connections before it accepts queries. Add a retry loop in before_script:

- |
  for i in $(seq 1 30); do
    php artisan db:show && break
    echo "Waiting for MySQL..."
    sleep 2
  done

On sister sites sharing Deployer 7 + GitLab CI, this retry block eliminated intermittent "Connection refused" failures during Dashain-week merge bursts when runners were under load.

Parallel execution and caching

Laravel's --parallel flag cuts integration runtime roughly in half on multi-core runners. Pair it with Composer vendor caching described in build caching to speed up CI builds. Do not cache bootstrap/cache across branches; stale config breaks tests silently.

  1. Cache vendor/ keyed by composer.lock hash
  2. Run php artisan config:clear at the start of every test job
  3. Split Feature and Unit into separate jobs for clearer failure signals
  4. Gate deploy on the integration job, not only on unit tests

What test data and secrets patterns work in integration pipelines?

Integration tests need predictable data without polluting shared state. RefreshDatabase or LazilyRefreshDatabase truncates tables between test classes. For large seed datasets, use factory states instead of loading production dumps—never commit real customer rows.

CI variables hold sandbox API keys for Khalti, eSewa, or Stripe test mode. Mask and protect them per handling secrets in CI/CD pipelines safely. Integration tests should call sandbox endpoints or use Http::fake() with recorded response fixtures stored in tests/Fixtures/.

Validate fixture JSON with a JSON formatter and validator before committing. One malformed webhook payload can make an entire suite fail with opaque parse errors.

For advanced scenarios—restoring anonymised dumps to verify migration paths—see database restore testing you should actually do and test data management for pipelines.

CI Job Network TopologyJob Containerphp:8.3-cli + Laravelphp artisan testmysql:8.4host: mysqlredis:8host: redisCI Variablesmasked secretsAll services share one Docker bridge networkUse service alias as hostname in .env.testing
Integration Testing in CI Pipelines connects the job container to MySQL and Redis via Docker network aliases.

Example integration test for a booking endpoint

<?php

namespace Tests\Feature;

use App\Models\Trip;
use Illuminate\Foundation\Testing\RefreshDatabase;
use Tests\TestCase;

class BookingIntegrationTest extends TestCase
{
    use RefreshDatabase;

    public function test_guest_can_hold_seats_until_payment(): void
    {
        $trip = Trip::factory()->create(['seats' => 10]);

        $response = $this->postJson("/api/trips/{$trip->id}/hold", [
            'seats' => 2,
            'email' => 'guest@example.com',
        ]);

        $response->assertStatus(201)
            ->assertJsonStructure(['hold_id', 'expires_at']);

        $this->assertDatabaseHas('seat_holds', [
            'trip_id' => $trip->id,
            'seats' => 2,
        ]);

        $this->assertEquals(8, $trip->fresh()->seats_available);
    }
}

This test fails if migrations, model accessors, or route middleware drift—all without a browser. That is the core value of Integration Testing in CI Pipelines.

How do you enforce quality gates and fix common CI integration failures?

Passing tests alone are not enough for mature pipelines. Add coverage thresholds on integration suites separately from units, as covered in code coverage gates in CI. Contract tests against consumer-driven schemas belong in the same stage; see API contract testing with Pact for microservice boundaries.

Common failure modes and fixes:

  • SQLSTATE connection refused — wrong DB_HOST; must be service alias mysql, not localhost
  • Migration already exists — stale volume; ensure ephemeral DB or drop schema in before_script
  • Class not found after deploy — run composer dump-autoload and clear config cache before tests
  • Redis connection timeout — confirm REDIS_CLIENT=phpredis and extension installed in job image
  • Flaky time assertions — use Carbon::setTestNow() instead of comparing raw timestamps

Bitbucket and Jenkins teams can mirror the same service-container pattern; platform syntax differs but topology does not. Compare runner options in Bitbucket Pipelines CI/CD guide and Jenkins CI/CD practical tutorial.

Defects Caught by StageWithout integration CIProduction bugs: highStaging onlyUnitWith integration CIUnitIntegration CIProdshift leftTypical fix cost curveCI failure: minutesStaging: hours to daysProduction: Rs 50k+~USD 375 incident cost
Integration Testing in CI Pipelines shifts database and API defects left, reducing costly production fixes.

PHPUnit's own guidance on test doubles versus real collaborators aligns with this split. Review the PHPUnit 11 manual when deciding what to mock inside integration jobs.

When to run the full suite

Run integration tests on every merge request and on default-branch pushes. Schedule heavy E2E suites nightly. For Nepal-based teams with limited runner minutes—often Rs 3,000–8,000/month (~USD 22–60) on shared GitLab SaaS—parallel jobs plus caching keep costs predictable. Deeper pipeline design belongs in build pipeline automation best practices.

If you lack dedicated DevOps staff, outsourcing pipeline hardening through testing and optimization services or Linux system administration often pays back after the first prevented outage. Custom Laravel platforms benefit from web development teams that treat CI as part of delivery, not an afterthought.

Key Takeaways

  • Place Integration Testing in CI Pipelines after unit tests and before deploy, using real MySQL and Redis service containers.
  • Set DB_HOST=mysql and REDIS_HOST=redis to match GitLab service aliases, never localhost inside job containers.
  • Use RefreshDatabase, factories, and masked CI variables—never production dumps or unmasked secrets.
  • Split unit and integration jobs, enable JUnit artefacts, and add a MySQL readiness retry loop.
  • Gate merges on integration results and reserve browser E2E tests for nightly or pre-release pipelines.
  • Cache vendor/ by lockfile hash and run config:clear every job to avoid stale bootstrap cache.

People Also Ask

Should integration tests run in the same job as unit tests?

Separate jobs give clearer failure signals and let unit tests finish in under a minute while integration work proceeds in parallel. Merge requests then show exactly which layer broke without scrolling a single long log.

Can you run integration tests without Docker?

Yes, but Docker-based service containers are the standard on GitLab, GitHub Actions, and Bitbucket. Shell runners can point at local MySQL instances, yet that reintroduces environment drift—the problem CI is meant to eliminate.

How many integration tests are enough?

Cover every critical write path: checkout, registration, document upload, payment callback, and scheduled job dispatch. Aim for high confidence on business rules, not 100% line coverage. Pair with shift-left security checks on the same pipeline for auth and input-validation regressions.

Does Laravel 13 change CI integration testing?

Laravel 13 requires PHP 8.3 minimum and keeps Pest and PHPUnit support unchanged at the pipeline level. Update your job image tag, run composer update, and re-run migrations in CI before merging framework bumps.

Ship integration tests before your next deploy

Integration Testing in CI Pipelines is the cheapest insurance against migration mistakes, queue misconfiguration, and payment webhook regressions. Start with one GitLab job, MySQL and Redis services, and a handful of Feature tests on your highest-risk routes. Expand coverage as failures teach you where mocks lied.

If you want a pipeline audit on an existing Laravel or legal-tech platform, contact us for a focused review. You can also browse the portfolio for production systems that run on Deployer 7 + GitLab CI, or read more on the blog about CI/CD pipeline setup. For background on how I approach delivery, see about me.

Frequently Asked Questions

It exercises application code against real MySQL, Redis, and mail fakes in CI job containers after unit tests and before deploy, catching wiring failures unit mocks never surface.

Unit tests mock the database; integration tests hit it. I've seen Laravel apps pass hundreds of unit tests, then fail in staging because a foreign key migration order was wrong. Running migrations against a fresh schema every pipeline run mirrors first deploy behaviour. Integration Testing in CI Pipelines also validates HTTP routes, queue dispatch, and payment webhook handling before Deployer swaps the release symlink. For booking systems, tests around reservation holds and supplier notifications prevent repeated staging fire drills when traffic or webhook volume grows.

The test pyramid still applies. Unit tests stay fast and isolated with no real dependencies, typically thirty to ninety seconds. Integration tests occupy the middle: two to eight minutes with MySQL, Redis, and mail fakes, exercising routes, Eloquent, jobs, and migrations without browser overhead. E2E browser suites using Dusk or Playwright run ten to thirty plus minutes and belong on nightly or pre-release schedules. In Laravel, most Feature tests are integration tests when they use RefreshDatabase or hit HTTP with Http::fake(). Pure unit tests never boot the full application kernel.

Most Laravel apps finish integration suites in two to eight minutes with MySQL and Redis service containers, versus thirty to ninety seconds for unit tests alone.

Add an integration_tests job in the test stage using a php:8.3-cli image with mysql:8.4 and redis:8 service containers aliased as mysql and redis. Set DB_HOST=mysql and REDIS_HOST=redis, copy .env.testing, run composer install with vendor caching, migrate with seed, then php artisan test --parallel --testsuite=Feature. Enable JUnit output in phpunit.xml so GitLab surfaces failing test names in merge requests. Add a MySQL readiness retry loop in before_script because TCP acceptance precedes query readiness. Gate deploy on this job passing, not only unit tests.

GitLab CI service containers share a Docker network with the job image. Your Laravel app connects to hostname mysql and redis, not 127.0.0.1. That single misconfiguration causes more pipeline failures than assertion mismatches. When DB_HOST points at localhost, PHPUnit feature tests fail with SQLSTATE connection refused even though MySQL is running beside the job. The same applies to REDIS_HOST=redis. On sister sites sharing Deployer 7 and GitLab CI, correcting alias hostnames eliminated intermittent connection failures during high merge-volume periods when runners were under load.

Separate jobs give clearer failure signals. Unit tests finish in under a minute while integration work proceeds in parallel on its own runner. Merge requests then show exactly which layer broke without scrolling one long log. Split Feature and Unit into distinct GitLab CI jobs, cache vendor by composer.lock hash, run php artisan config:clear at the start of every test job, and avoid caching bootstrap/cache across branches because stale config breaks tests silently. Gate merges on integration results specifically, not only on fast unit checks that never touch MySQL or Redis.

Yes, but Docker-based service containers are the standard on GitLab, GitHub Actions, and Bitbucket. Shell runners can point at local MySQL instances, yet that reintroduces environment drift, the problem CI is meant to eliminate. Bitbucket and Jenkins teams can mirror the same service-container topology; only platform syntax differs. For predictable Integration Testing in CI Pipelines, ephemeral MySQL and Redis beside the job image remain the practical default on GitLab runners used before Deployer deployments.

Cover HTTP feature tests that POST to routes and assert database state, queue job dispatch with Redis while using Queue::fake() only where isolation demands it, migration runs against an empty schema on every pipeline execution, payment gateway sandbox calls with credentials from masked CI variables, and file upload flows using the local disk driver inside the job container. A booking endpoint test that creates a trip via factory, posts a hold request, and asserts seat_holds rows catches migration drift, model accessor bugs, and middleware regressions without launching a browser.

Use RefreshDatabase or LazilyRefreshDatabase to truncate tables between test classes. Prefer factory states over large seed datasets and never commit production dumps or real customer rows. Store sandbox API keys for Khalti, eSewa, or Stripe test mode in masked, protected CI variables. Call sandbox endpoints or use Http::fake() with recorded response fixtures in tests/Fixtures/. Validate fixture JSON before committing because one malformed webhook payload can fail an entire suite with opaque parse errors. Production data has no place in pipeline databases.

SQLSTATE connection refused means wrong DB_HOST; set the service alias mysql, not localhost. Migration already exists signals a stale volume; use an ephemeral database or drop the schema in before_script. Class not found after deploy requires composer dump-autoload and config cache clearing before tests. Redis connection timeout needs REDIS_CLIENT=phpredis and the redis PHP extension in the job image. Flaky timestamp assertions should use Carbon::setTestNow() instead of comparing raw timestamps. Add a MySQL readiness retry loop when connections fail intermittently under runner load.

Run them on every merge request and default-branch push, placed after unit tests and before deploy; schedule heavy browser E2E suites nightly instead.

Cover every critical write path: checkout, registration, document upload, payment callback, and scheduled job dispatch. Aim for high confidence on business rules, not one hundred percent line coverage. Pair integration suites with contract tests against consumer-driven schemas for API boundaries and separate coverage thresholds from unit tests. Start with a handful of Feature tests on highest-risk routes and expand as failures reveal where mocks lied. One GitLab job with MySQL, Redis, and targeted Feature tests beats a large brittle suite nobody trusts.

Laravel 13 requires PHP 8.3 minimum and keeps Pest and PHPUnit support unchanged at the pipeline level. Update your job image tag from php:8.3-cli, run composer update, and re-run migrations in CI before merging framework bumps. Pipeline topology stays the same: service containers for MySQL and Redis, RefreshDatabase in Feature tests, parallel execution with php artisan test --parallel, and JUnit artefacts for merge request visibility. Integration Testing in CI Pipelines does not need a different stage order when moving from Laravel 12 to 13.

Nepal-based teams on shared GitLab SaaS often spend Rs 3,000 to 8,000 per month, roughly USD 22 to 60, on runner minutes. Parallel jobs plus Composer vendor caching keep costs predictable compared with running everything serially. Integration suites at two to eight minutes per run cost far less than production fixes for migration mistakes, queue misconfiguration, or payment webhook regressions caught only after merge. Reserve expensive browser E2E suites for nightly schedules to protect runner budgets while still gating deploys on real-service integration results.

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: