Build CoreOrdered learning track

Compose for Testing: Integration, Contract, E2E, Fixture, and Ephemeral Stacks

Learn Docker, Containerization, Docker Compose, Docker Swarm - Part 019

Advanced Docker Compose testing: integration test harness, contract test, E2E topology, fixture management, ephemeral stacks, CI execution, parallel isolation, readiness gates, and failure-mode discipline.

15 min read2819 words
PrevNext
Lesson 1935 lesson track0719 Build Core
#docker#containerization#docker-compose#testing+3 more

Part 019 — Compose for Testing: Integration, Contract, E2E, Fixture, and Ephemeral Stacks

Target pembelajaran: setelah part ini, kita mampu memakai Docker Compose sebagai test harness yang repeatable, isolated, deterministic, dan cocok untuk integration test, contract test, E2E, serta CI pipeline tanpa bergantung pada dependency lokal yang tidak terkendali.

Part 018 membahas Compose untuk development inner loop. Part ini membahas Compose sebagai execution environment untuk testing.

Mental model utama:

Compose testing bukan sekadar menjalankan dependency.
Compose testing adalah cara mendefinisikan boundary sistem, fixture, readiness, lifecycle, cleanup, dan observability test.

Docker Compose secara resmi dapat digunakan di development, testing, staging, production, dan CI workflow. Namun Compose tidak otomatis membuat test reliable. Reliability muncul dari desain environment, state, startup, dan failure handling yang disiplin.


1. Mengapa Testing dengan Compose Penting?

Unit test cepat, tetapi banyak defect sistem modern muncul di boundary:

  • database schema mismatch;
  • transaction isolation behavior;
  • connection pool exhaustion;
  • DNS/service discovery error;
  • wrong environment variable;
  • queue redelivery behavior;
  • cache invalidation bug;
  • object storage endpoint mismatch;
  • TLS/certificate problem;
  • startup race;
  • permission/UID issue;
  • port collision;
  • migration ordering issue.

Compose membantu karena kita bisa mendefinisikan dependency eksternal sebagai container:

Tetapi ada jebakan:

Jika Compose test stack tidak deterministic, ia hanya memindahkan flakiness dari laptop ke CI.

2. Posisi Compose dalam Testing Pyramid

Compose cocok untuk level test yang memerlukan real dependency.

Compose bukan pengganti unit test. Compose berguna ketika behavior hanya bisa divalidasi dengan proses, network, filesystem, atau dependency nyata.

Test TypeCompose ValueRisiko
Unit testrendahoverhead terlalu besar
Repository integration testtinggifixture DB harus rapi
Messaging integration testtinggirace dan eventual consistency
Contract testtinggiversioning contract harus disiplin
E2E testtinggiruntime lambat dan flaky jika readiness buruk
Load/smoke testsedang-tinggilaptop/CI resource bisa misleading

3. Mental Model: Test Environment sebagai State Machine

Test environment punya lifecycle.

Failure paling umum terjadi karena engineer menganggap environment langsung siap setelah container started.

Invariant yang benar:

started != ready
ready != seeded
seeded != test-safe

4. Bentuk Topologi Compose untuk Testing

Ada beberapa pola.

4.1 Dependency-Only Stack

Aplikasi dijalankan di host, dependency di Compose.

Cocok untuk:

  • inner loop cepat;
  • IDE debugging mudah;
  • bahasa/framework yang butuh hot reload host;
  • repository integration test lokal.

Kelemahan:

  • host dependency bocor;
  • environment tidak identik dengan CI;
  • port publish diperlukan;
  • konflik port lebih mungkin.

4.2 Full In-Compose Stack

Aplikasi dan test runner berjalan di Compose.

Cocok untuk:

  • CI;
  • clean-room validation;
  • onboarding;
  • test yang harus bebas dari dependency host;
  • release candidate validation.

Kelemahan:

  • build image harus benar;
  • debugging lebih kompleks;
  • cache strategi penting;
  • artifact test harus diekstrak.

4.3 Hybrid Stack

Aplikasi utama di host, dependency dan beberapa tools di Compose.

Cocok untuk development sehari-hari, tetapi jangan jadikan satu-satunya quality gate.


5. Canonical Compose Test Stack

Contoh dasar:

# compose.test.yml
services:
  api:
    build:
      context: .
      target: runtime
    environment:
      APP_ENV: test
      DATABASE_URL: postgres://app:app@postgres:5432/app_test
      REDIS_URL: redis://redis:6379/0
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy
    networks:
      - testnet

  test-runner:
    build:
      context: .
      target: test
    environment:
      APP_BASE_URL: http://api:8080
      DATABASE_URL: postgres://app:app@postgres:5432/app_test
    depends_on:
      api:
        condition: service_healthy
    command: ["./scripts/test-integration.sh"]
    networks:
      - testnet

  postgres:
    image: postgres:16
    environment:
      POSTGRES_USER: app
      POSTGRES_PASSWORD: app
      POSTGRES_DB: app_test
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U app -d app_test"]
      interval: 2s
      timeout: 2s
      retries: 30
    tmpfs:
      - /var/lib/postgresql/data
    networks:
      - testnet

  redis:
    image: redis:7
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 2s
      timeout: 2s
      retries: 30
    networks:
      - testnet

networks:
  testnet: {}

Catatan penting:

  • tmpfs membuat database ephemeral untuk test cepat dan bersih;
  • test-runner tidak expose port ke host;
  • service saling akses via DNS Compose, bukan localhost;
  • readiness dependency diekspresikan via healthcheck;
  • state test tidak survive antar run.

6. Cara Menjalankan Test Stack

Pola CI umum:

docker compose -f compose.test.yml up \
  --build \
  --abort-on-container-exit \
  --exit-code-from test-runner

Makna operasional:

FlagTujuan
--buildmemastikan image sesuai source saat ini
--abort-on-container-exithentikan stack saat test selesai/gagal
--exit-code-from test-runnerexit code pipeline mengikuti test runner

Tambahkan cleanup:

docker compose -f compose.test.yml down -v --remove-orphans

Invariant CI:

exit code harus berasal dari test runner, bukan dari service dependency.
cleanup harus menghapus volume agar state tidak bocor.

7. Dependency Readiness: Jangan Percaya Sleep

Anti-pattern:

command: ["sh", "-c", "sleep 10 && npm test"]

Masalah:

  • terlalu pendek saat CI lambat;
  • terlalu panjang saat lokal cepat;
  • tidak memvalidasi dependency benar-benar siap;
  • menyembunyikan failure sebenarnya.

Lebih baik:

postgres:
  healthcheck:
    test: ["CMD-SHELL", "pg_isready -U app -d app_test"]
    interval: 2s
    timeout: 2s
    retries: 30

test-runner:
  depends_on:
    postgres:
      condition: service_healthy

Namun healthcheck juga harus benar. pg_isready membuktikan server menerima koneksi, bukan membuktikan schema sudah migrated.

Karena itu readiness sering perlu dua tahap:

DB accepting connection -> migrations completed -> fixtures loaded -> tests start

8. Migration sebagai Job, Bukan Efek Samping Gelap

Untuk sistem database-backed, migration harus explicit.

migrate:
  build:
    context: .
    target: migration
  environment:
    DATABASE_URL: postgres://app:app@postgres:5432/app_test
  depends_on:
    postgres:
      condition: service_healthy
  command: ["./scripts/migrate.sh"]
  restart: "no"

api:
  depends_on:
    migrate:
      condition: service_completed_successfully

Pola ini membuat urutan valid:

Keuntungan:

  • migration failure terlihat jelas;
  • API tidak start dengan schema kosong;
  • test-runner tidak perlu menebak state DB;
  • CI log lebih mudah dibaca.

9. Fixture Management

Fixture adalah data awal test.

Ada tiga strategi umum.

9.1 Seed Once Per Stack

seed:
  build:
    context: .
    target: test
  environment:
    DATABASE_URL: postgres://app:app@postgres:5432/app_test
  depends_on:
    migrate:
      condition: service_completed_successfully
  command: ["./scripts/seed-test-data.sh"]
  restart: "no"

Cocok untuk E2E smoke test.

Risiko:

  • test saling bergantung pada fixture global;
  • urutan test bisa memengaruhi hasil;
  • sulit parallel.

9.2 Seed Per Test Suite

Test runner mengatur fixture sebelum suite.

Cocok untuk repository/service integration test.

Risiko:

  • membutuhkan helper test yang baik;
  • cleanup harus rapi.

9.3 Test Data Builder

Test membuat data yang dibutuhkan sendiri.

Cocok untuk test skala besar.

Kelemahan:

  • lebih banyak kode test;
  • butuh factory/helper.

Rule praktis:

Global fixture boleh untuk reference data.
Scenario data sebaiknya dibuat oleh test scenario.

10. Ephemeral vs Persistent State untuk Test

10.1 Ephemeral State

Gunakan tmpfs atau anonymous volume yang dihapus setelah run.

postgres:
  tmpfs:
    - /var/lib/postgresql/data

Keuntungan:

  • bersih;
  • cepat;
  • tidak drift;
  • cocok CI.

Kelemahan:

  • data hilang saat container mati;
  • tidak cocok untuk debugging panjang.

10.2 Persistent State

Gunakan named volume.

volumes:
  pgdata_test:

services:
  postgres:
    volumes:
      - pgdata_test:/var/lib/postgresql/data

Keuntungan:

  • bisa inspeksi manual;
  • startup lebih cepat jika DB berat;
  • cocok untuk local debug.

Kelemahan:

  • state drift;
  • test bisa false positive;
  • cleanup wajib.

Rekomendasi:

ContextState Strategy
CIephemeral
local quick integrationephemeral by default
local debugging failurepersistent optional profile
performance investigationpersistent controlled dataset

11. Parallel Test Isolation

CI modern sering menjalankan test paralel.

Problem:

  • project name sama;
  • network name konflik;
  • volume name konflik;
  • published port konflik;
  • database name sama;
  • container name hardcoded konflik.

Gunakan COMPOSE_PROJECT_NAME unik.

export COMPOSE_PROJECT_NAME="myapp_test_${CI_JOB_ID:-local_$(date +%s)}"
docker compose -f compose.test.yml up --build --abort-on-container-exit --exit-code-from test-runner
docker compose -f compose.test.yml down -v --remove-orphans

Jangan hardcode container_name di test Compose.

Anti-pattern:

services:
  postgres:
    container_name: postgres

Kenapa buruk:

  • merusak scaling;
  • konflik antar project;
  • konflik antar developer;
  • konflik antar CI job.

Compose sudah memberi DNS service name. Gunakan postgres, bukan nama container global.


12. Port Strategy untuk Test

Dalam Compose network, service tidak perlu publish port ke host untuk saling bicara.

api:
  expose:
    - "8080"

Atau bahkan tidak perlu expose; service tetap bisa diakses oleh service lain lewat port listen internal.

Publish hanya jika host perlu akses:

api:
  ports:
    - "127.0.0.1:18080:8080"

Untuk test paralel, hindari static published ports.

Alternatif:

ports:
  - "127.0.0.1::8080"

Lalu baca port dari docker compose port jika host test runner membutuhkan.

Namun untuk CI paling bersih:

test-runner berjalan di network Compose, sehingga tidak perlu published port sama sekali.

13. Contract Testing dengan Compose

Contract testing memvalidasi compatibility antar service tanpa harus menjalankan seluruh production topology.

Contoh consumer contract test:

Contoh provider verification:

Compose value:

  • menjalankan provider dengan dependency nyata;
  • menjalankan verifier sebagai container;
  • menutup gap environment lokal vs CI;
  • membuat contract verification reproducible.

Contoh Compose:

services:
  provider:
    build:
      context: .
      target: runtime
    environment:
      DATABASE_URL: postgres://app:app@postgres:5432/provider_test
    depends_on:
      postgres:
        condition: service_healthy

  contract-verifier:
    build:
      context: .
      target: test
    command: ["./scripts/verify-contracts.sh"]
    environment:
      PROVIDER_BASE_URL: http://provider:8080
      CONTRACT_DIR: /contracts
    volumes:
      - ./contracts:/contracts:ro
    depends_on:
      provider:
        condition: service_healthy

Critical invariant:

Contract test harus menguji compatibility boundary, bukan seluruh user journey.

14. E2E Testing dengan Compose

E2E test memvalidasi user journey.

Topologi umum:

Compose example:

services:
  frontend:
    build:
      context: ./frontend
      target: runtime
    environment:
      API_BASE_URL: http://api:8080
    depends_on:
      api:
        condition: service_healthy

  api:
    build:
      context: ./backend
      target: runtime
    environment:
      DATABASE_URL: postgres://app:app@postgres:5432/app_test
    depends_on:
      migrate:
        condition: service_completed_successfully
    healthcheck:
      test: ["CMD", "curl", "-fsS", "http://localhost:8080/health/ready"]
      interval: 2s
      timeout: 2s
      retries: 30

  e2e:
    build:
      context: ./e2e
    command: ["npx", "playwright", "test"]
    environment:
      BASE_URL: http://frontend:3000
    depends_on:
      frontend:
        condition: service_healthy

Jangan menjalankan E2E terlalu banyak di PR jika lambat. Gunakan stratifikasi:

Pipeline StageTest
pre-commit/localunit + focused integration
PRunit + integration + contract + smoke E2E
main branchbroader E2E
nightlyfull E2E + destructive scenarios

15. Service Virtualization vs Real Dependency

Tidak semua dependency harus nyata.

DependencyReal ContainerStub/MockCatatan
Postgres/MySQLsangat baikburuk untuk SQL behaviorreal DB lebih bernilai
Redisbaikbisa mock untuk unitreal untuk TTL/eviction behavior
Kafka/RabbitMQbaik tapi beratberguna untuk consumer logicreal untuk ordering/redelivery
S3/object storageemulator cukupmock untuk unitemulator untuk integration
Payment gatewayjangan real by defaultstub/sandboxcontract penting
Email providermail catcherstubhindari email nyata

Mental model:

Gunakan dependency nyata jika protocol/semantics penting.
Gunakan stub jika dependency eksternal mahal, tidak deterministic, atau berbahaya.

16. Test Runner Image Design

Test runner harus menjadi image first-class.

Dockerfile pattern:

FROM eclipse-temurin:21-jdk AS build
WORKDIR /workspace
COPY gradlew settings.gradle build.gradle ./
COPY gradle ./gradle
RUN ./gradlew dependencies --no-daemon
COPY src ./src
RUN ./gradlew assemble --no-daemon

FROM build AS test
COPY scripts ./scripts
CMD ["./gradlew", "test", "integrationTest", "--no-daemon"]

FROM eclipse-temurin:21-jre AS runtime
WORKDIR /app
COPY --from=build /workspace/build/libs/app.jar /app/app.jar
USER 10001:10001
ENTRYPOINT ["java", "-jar", "/app/app.jar"]

Keuntungan:

  • CI tidak perlu install JDK/Node/Python dependency secara manual;
  • dependency test version-controlled;
  • clean-room reproducibility;
  • image build cache bisa dimanfaatkan.

Kelemahan:

  • image test bisa besar;
  • build time harus dioptimalkan;
  • cache dependency harus dirancang.

17. Artifact Test: Reports, Screenshots, Coverage

Test runner sering menghasilkan artifact:

  • JUnit XML;
  • coverage report;
  • screenshot E2E;
  • video E2E;
  • trace file;
  • logs;
  • DB dump after failure.

Mount output directory:

test-runner:
  volumes:
    - ./build/test-results:/workspace/build/test-results
    - ./build/reports:/workspace/build/reports

Untuk CI, pastikan path artifact stabil.

Lebih aman:

volumes:
  test_artifacts:

services:
  test-runner:
    volumes:
      - test_artifacts:/artifacts

Lalu copy setelah failure:

docker compose -f compose.test.yml cp test-runner:/artifacts ./artifacts || true

18. Diagnostics saat Test Gagal

Pipeline yang baik tidak hanya memberi exit code. Ia memberi konteks.

Script contoh:

#!/usr/bin/env bash
set -euo pipefail

COMPOSE_FILE="compose.test.yml"
PROJECT="${COMPOSE_PROJECT_NAME:-myapp_test_local}"

cleanup() {
  docker compose -p "$PROJECT" -f "$COMPOSE_FILE" down -v --remove-orphans || true
}

diagnostics() {
  echo "--- compose ps ---"
  docker compose -p "$PROJECT" -f "$COMPOSE_FILE" ps || true

  echo "--- compose logs ---"
  docker compose -p "$PROJECT" -f "$COMPOSE_FILE" logs --no-color || true

  echo "--- docker events recent not captured here; inspect failed containers manually if needed ---"
}

trap cleanup EXIT

if ! docker compose -p "$PROJECT" -f "$COMPOSE_FILE" up --build --abort-on-container-exit --exit-code-from test-runner; then
  diagnostics
  exit 1
fi

Principle:

Failure without diagnostics creates rerun culture.
Rerun culture hides flaky systems.

19. Flakiness Taxonomy

Compose test flakiness biasanya berasal dari salah satu sumber ini.

SymptomLikely CauseFix
test kadang gagal koneksi DBreadiness salahhealthcheck + migration job
test gagal di CI sajaresource/CPU lambattimeout realistis + readiness explicit
port already allocatedstatic portno publish / dynamic port / unique project
data test tercampurvolume tidak dihapusdown -v, tmpfs
service tidak resolvesalah DNS/localhostpakai service name
test order dependentshared fixture mutableisolate scenario data
queue test flakyasync wait salahawait condition, bukan sleep
E2E flakyUI readiness salahexplicit wait pada user-visible state
image tidak updatestale build/cache--build, cache policy

20. Async Dependency Testing

Queue, stream, dan event-driven system butuh strategi khusus.

Anti-pattern:

publish event -> sleep 5 seconds -> assert database

Lebih baik:

publish event -> poll observed condition with timeout -> assert final state

Pseudo-code:

await()
  .atMost(Duration.ofSeconds(30))
  .pollInterval(Duration.ofMillis(250))
  .untilAsserted(() -> {
      Order order = repository.findById(orderId);
      assertThat(order.status()).isEqualTo(OrderStatus.CONFIRMED);
  });

Compose tidak menyelesaikan eventual consistency. Compose hanya menyediakan environment. Test tetap harus memahami semantics async.


21. Database Testing Discipline

21.1 Schema Source of Truth

Schema test harus berasal dari migration yang sama dengan production.

Anti-pattern:

production uses Flyway migration
integration test uses Hibernate ddl-auto create

Masalah:

  • test schema berbeda dari production;
  • index/constraint bisa hilang;
  • migration bug tidak tertangkap.

Better:

start DB -> run migrations -> run tests

21.2 Transaction Rollback Strategy

Untuk repository tests, rollback per test bisa cepat.

Untuk full service tests, rollback tidak selalu mudah karena request berjalan di process lain. Gunakan:

  • scenario-specific data;
  • truncate tables between tests;
  • ephemeral DB per suite;
  • unique tenant/test namespace;
  • per-test database jika parallel tinggi.

21.3 Performance Trap

Test DB di tmpfs bisa lebih cepat dari production. Jangan ambil kesimpulan performance production dari Compose test lokal.


22. Compose Profiles untuk Test Variants

Gunakan profiles untuk dependency opsional.

services:
  mailpit:
    image: axllent/mailpit
    profiles: ["e2e", "mail"]

  object-storage:
    image: minio/minio
    profiles: ["e2e", "storage"]

Run:

docker compose -f compose.test.yml --profile e2e up --build --abort-on-container-exit --exit-code-from e2e

Rule:

Default test stack harus minimal.
Expensive dependencies harus opt-in.

23. Multiple Compose Files untuk Test

Common pattern:

compose.yml
compose.dev.yml
compose.test.yml
compose.e2e.yml
compose.ci.yml

Contoh:

docker compose \
  -f compose.yml \
  -f compose.test.yml \
  -f compose.ci.yml \
  up --build --abort-on-container-exit --exit-code-from test-runner

Gunakan layering untuk override:

  • command test;
  • environment test;
  • tmpfs volume;
  • no published ports;
  • lower resource consumption;
  • diagnostic labels.

Jangan membuat file override terlalu banyak sampai sulit memahami final config.

Selalu inspect final model:

docker compose -f compose.yml -f compose.test.yml config

24. Secrets dan Sensitive Data di Test

Test environment sering menjadi jalur kebocoran secret.

Rules:

  • jangan pakai production secret untuk test;
  • jangan commit real token di Compose file;
  • gunakan fake credential untuk dependency lokal;
  • gunakan secret manager CI untuk credential sandbox;
  • pastikan logs tidak mencetak token;
  • cleanup artifact sebelum publish.

Compose test credential sebaiknya disposable:

environment:
  PAYMENT_API_KEY: test_fake_key_do_not_use_in_prod

Untuk sandbox eksternal:

environment:
  PAYMENT_API_KEY: ${PAYMENT_SANDBOX_API_KEY:?missing sandbox key}

25. CI Pipeline Pattern

Contoh GitHub Actions-style logic secara konseptual:

steps:
  - checkout
  - setup docker buildx
  - docker compose config validation
  - docker compose build
  - docker compose up integration stack
  - upload test artifacts
  - docker compose down -v

Command detail:

set -euo pipefail

export COMPOSE_PROJECT_NAME="myapp_${GITHUB_RUN_ID:-local}_${GITHUB_RUN_ATTEMPT:-0}"

docker compose -f compose.test.yml config --quiet

docker compose -f compose.test.yml build

docker compose -f compose.test.yml up \
  --abort-on-container-exit \
  --exit-code-from test-runner

docker compose -f compose.test.yml down -v --remove-orphans

Important invariant:

CI harus menjalankan compose config validation sebelum menjalankan stack.

26. Resource Management untuk Test Stack

CI runner sering punya resource terbatas.

Gejala:

  • OOM kill;
  • DB startup lambat;
  • browser E2E crash;
  • build timeout;
  • healthcheck timeout;
  • queue broker lambat.

Strategi:

  • gunakan image dependency ringan;
  • aktifkan hanya service yang dibutuhkan;
  • pisahkan E2E berat dari PR fast path;
  • set timeout realistis;
  • batasi parallelism test;
  • gunakan cache dependency build;
  • collect docker stats saat debugging.

Compose testing harus punya budget:

PR integration stack harus selesai dalam menit, bukan puluhan menit.

27. Test Data for Multi-Service Systems

Dalam microservice atau modular monolith, data test bisa tersebar.

Risiko:

  • fixture cross-service tidak konsisten;
  • event replay urutan salah;
  • service A seeded, service B belum;
  • test tahu terlalu banyak internal dependency.

Solusi:

  • seed via public API jika testing user journey;
  • seed DB langsung hanya untuk lower-level integration;
  • gunakan scenario builder yang eksplisit;
  • record fixture version;
  • gunakan contract untuk boundary antar service.

28. Testing with External Dependencies

Kadang dependency tidak bisa di-container-kan:

  • payment sandbox;
  • identity provider;
  • SaaS API;
  • managed database;
  • proprietary service.

Compose tetap bisa membantu dengan network + test runner, tetapi reliability bergantung pada eksternal.

Decision matrix:

External DependencyPR PipelineNightlyManual
unstable sandboxstubrealreal
critical integrationcontract + stubrealreal
costly APIstubselectiveselective
irreversible operationfake onlysandbox with guardmanual approval

Guardrail:

Test otomatis tidak boleh memanggil production external endpoint.

29. Image Build Strategy untuk Test

Ada dua model.

29.1 Build Inside Test Run

docker compose -f compose.test.yml up --build

Cocok untuk lokal dan simple CI.

29.2 Build Once, Test Many

docker buildx build -t registry/app:${GIT_SHA} --load .
docker compose -f compose.test.yml up

Compose file:

services:
  api:
    image: registry/app:${GIT_SHA}

Cocok untuk pipeline matang:

  • build artifact immutable;
  • scan image;
  • run tests against same image;
  • promote same digest.

Rule top 1%:

Test yang memvalidasi release harus menjalankan image yang sama dengan kandidat release.

30. Observability for Test Harness

Compose test stack perlu observability minimal:

  • logs semua service;
  • health status;
  • exit code;
  • timing startup;
  • artifact test;
  • dependency version;
  • final Compose config;
  • image digest jika memungkinkan.

Tambahkan labels:

services:
  api:
    labels:
      com.example.stack: test
      com.example.service: api
      com.example.git_sha: ${GIT_SHA:-local}

Saat incident test flaky, label membantu filtering.


31. Security Boundary untuk Test Stack

Jangan karena “hanya test” lalu menjalankan semuanya privileged.

Hindari:

privileged: true
volumes:
  - /var/run/docker.sock:/var/run/docker.sock

Kecuali memang sedang testing Docker-in-Docker / tooling container yang membutuhkan itu, dan isolated runner-nya aman.

Praktik aman:

  • no privileged by default;
  • bind mount read-only jika hanya baca;
  • jangan mount home directory penuh;
  • jangan expose DB ke 0.0.0.0;
  • gunakan fake secrets;
  • cleanup volumes;
  • jangan publish test services ke public interface.

32. Compose vs Testcontainers

Untuk engineer Java, pertanyaan umum: Compose atau Testcontainers?

KriteriaComposeTestcontainers
Multi-service app graphsangat baikbisa, tapi kode test lebih kompleks
Per-test container lifecyclekurang naturalsangat baik
Developer environment reusesangat baikkurang sebagai dev stack
CI parity dengan app stackbaikbaik untuk integration tests
Dynamic portsmanualbuilt-in
Test code controls dependencykurangsangat baik
Cross-language standardizationbaikbergantung ecosystem

Keduanya bisa digabung:

  • Compose untuk full stack/E2E;
  • Testcontainers untuk repository/service integration test yang perlu isolated dependency per test class.

Rule:

Jika environment adalah produk tim/platform, Compose kuat.
Jika dependency lifecycle adalah bagian dari test code, Testcontainers kuat.

33. Common Anti-Patterns

33.1 One Giant Compose for Every Test

Semua service naik untuk setiap test.

Dampak:

  • lambat;
  • flaky;
  • sulit debug;
  • resource boros.

Better: profiles dan layered Compose files.

33.2 Static Ports Everywhere

Dampak:

  • test parallel gagal;
  • developer konflik;
  • CI rerun flaky.

Better: internal network only.

33.3 Test Depends on Old Volume

Dampak:

  • false positive;
  • onboarding gagal;
  • CI lokal beda.

Better: down -v, tmpfs, reset script.

33.4 Sleep-Based Readiness

Dampak:

  • flaky dan lambat.

Better: healthcheck + wait-for-condition.

33.5 Production Secrets in Test

Dampak:

  • security incident.

Better: fake/sandbox credentials.


34. Review Checklist

Sebelum Compose test stack dianggap production-grade untuk engineering team, cek:

[ ] Compose config bisa divalidasi dengan `docker compose config --quiet`
[ ] Tidak ada `container_name` hardcoded
[ ] Tidak ada static published port kecuali perlu
[ ] Semua dependency penting punya healthcheck
[ ] Migration/seed job explicit
[ ] Test runner exit code menjadi pipeline exit code
[ ] Cleanup memakai `down -v --remove-orphans`
[ ] Project name unik di CI
[ ] Secrets test tidak memakai production secret
[ ] Artifact test bisa diambil setelah failure
[ ] Logs service tersedia saat gagal
[ ] State test ephemeral by default
[ ] Profiles dipakai untuk dependency mahal/opsional
[ ] Final test release menjalankan image kandidat release

35. Lab Praktik

Lab 1 — Dependency-Only Integration Test

Buat compose.test.yml untuk Postgres + Redis.

Kriteria:

  • aplikasi/test berjalan di host;
  • DB dan Redis punya healthcheck;
  • state ephemeral;
  • reset dengan satu command.

Lab 2 — Full In-Compose Test Runner

Tambahkan test-runner container.

Kriteria:

  • pipeline exit code mengikuti test-runner;
  • tidak ada published port;
  • docker compose up cukup untuk menjalankan test.

Lab 3 — Migration and Seed Jobs

Pisahkan:

  • migrate job;
  • seed job;
  • api service;
  • test-runner.

Kriteria:

  • api tidak start sebelum migration sukses;
  • test tidak start sebelum API ready;
  • seed failure menghentikan pipeline.

Lab 4 — Parallel CI Simulation

Jalankan dua stack paralel dengan project name berbeda.

COMPOSE_PROJECT_NAME=myapp_test_1 docker compose -f compose.test.yml up --abort-on-container-exit --exit-code-from test-runner &
COMPOSE_PROJECT_NAME=myapp_test_2 docker compose -f compose.test.yml up --abort-on-container-exit --exit-code-from test-runner &
wait

Kriteria:

  • tidak ada port conflict;
  • tidak ada volume conflict;
  • semua stack cleanup bersih.

36. Rubric Penguasaan

LevelIndikator
Juniorbisa menjalankan DB dependency dengan Compose
Intermediatepunya healthcheck, test runner, dan cleanup script
Seniormengelola migration, fixture, artifact, project isolation, dan CI exit code
Staff+mendesain Compose test harness sebagai platform testing repeatable dengan failure diagnostics, parallel isolation, security guardrail, dan release-image validation

37. Ringkasan

Compose testing yang kuat memiliki invariant:

Environment harus disposable.
Readiness harus explicit.
Migration dan seed harus explicit.
Exit code harus berasal dari test runner.
Cleanup harus menghapus state.
Parallel run harus isolated.
Port publish harus dihindari kecuali perlu.
Artifact dan logs harus tersedia saat failure.
Test release harus berjalan pada image kandidat release.

Part berikutnya membahas Compose production boundaries: kapan Compose cukup untuk single-host production, apa batas HA/scaling/orchestration-nya, bagaimana menggunakan override production, dan kapan harus naik ke orchestrator seperti Swarm/Kubernetes.


References

Lesson Recap

You just completed lesson 19 in build core. Use the series map if you want to review the broader track, or continue directly into the next lesson while the context is still warm.

Continue The Track

Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.