← Back to libraryQuestion 231 of 273
🔧CI/CDIntermediate

Service Containers (DB/API for Integration Tests)

📌 Definition:

Service containers are dependencies — a database, cache, message broker, or a stubbed API — that the CI system starts as containers alongside your test job, giving integration tests a real, ephemeral backend that is created fresh for the run and torn down afterward.

📖 Detailed Explanation:

Integration and API tests often need a real database or dependent service, not a mock. Pointing them at a shared, long-lived staging database is a recipe for flakiness: tests collide on shared data, one run's leftovers break the next, and the shared instance becomes a bottleneck and single point of failure. Service containers solve this by spinning up a dedicated, disposable instance (e.g. Postgres) scoped to the job. The test job waits for the service to be healthy, runs migrations/seed against it, executes tests, and the container is discarded — perfect isolation and repeatability. Each parallel job gets its own service, so parallelism doesn't cause data collisions. This is the sweet spot between fully mocking (fast but not testing the real integration) and using shared infrastructure (realistic but flaky and contended).

🔑 Key Points:
  • CI starts dependencies (DB, cache, broker) as ephemeral containers per job
  • Fresh, isolated backend per run — no leftover data, no cross-run collisions
  • Each parallel job gets its own service, so parallelism stays safe
  • Wait for health, run migrations/seed, test, then the container is discarded
  • Middle ground between pure mocks (fast) and shared staging infra (flaky/contended)
🌍 Real-World Example:

API integration tests used to hit a shared staging Postgres and flaked whenever two pipelines ran at once, colliding on the same rows. Switching to a Postgres service container per job gives each run its own clean database seeded from scratch, so concurrent pipelines never interfere and results are deterministic.

📎 Code Example:
jobs:
  integration:
    runs-on: ubuntu-latest
    services:
      postgres:
        image: postgres:16
        env:
          POSTGRES_PASSWORD: test
          POSTGRES_DB: appdb
        ports: ['5432:5432']
        options: >-
          --health-cmd="pg_isready" --health-interval=5s
          --health-timeout=5s --health-retries=5
    steps:
      - uses: actions/checkout@v4
      - run: npm ci
      - run: npm run db:migrate && npm run db:seed
        env: { DATABASE_URL: 'postgres://postgres:test@localhost:5432/appdb' }
      - run: npm run test:integration
        env: { DATABASE_URL: 'postgres://postgres:test@localhost:5432/appdb' }
🎯 Scenario-Based Interview Question:

Scenario: Integration tests point at a shared staging database. They flake whenever multiple pipelines run concurrently (duplicate-key and 'row not found' errors), and occasionally corrupt staging data for the whole team. How do you redesign this?