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.
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 Type | Compose Value | Risiko |
|---|---|---|
| Unit test | rendah | overhead terlalu besar |
| Repository integration test | tinggi | fixture DB harus rapi |
| Messaging integration test | tinggi | race dan eventual consistency |
| Contract test | tinggi | versioning contract harus disiplin |
| E2E test | tinggi | runtime lambat dan flaky jika readiness buruk |
| Load/smoke test | sedang-tinggi | laptop/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:
tmpfsmembuat database ephemeral untuk test cepat dan bersih;test-runnertidak 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:
| Flag | Tujuan |
|---|---|
--build | memastikan image sesuai source saat ini |
--abort-on-container-exit | hentikan stack saat test selesai/gagal |
--exit-code-from test-runner | exit 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:
| Context | State Strategy |
|---|---|
| CI | ephemeral |
| local quick integration | ephemeral by default |
| local debugging failure | persistent optional profile |
| performance investigation | persistent 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 Stage | Test |
|---|---|
| pre-commit/local | unit + focused integration |
| PR | unit + integration + contract + smoke E2E |
| main branch | broader E2E |
| nightly | full E2E + destructive scenarios |
15. Service Virtualization vs Real Dependency
Tidak semua dependency harus nyata.
| Dependency | Real Container | Stub/Mock | Catatan |
|---|---|---|---|
| Postgres/MySQL | sangat baik | buruk untuk SQL behavior | real DB lebih bernilai |
| Redis | baik | bisa mock untuk unit | real untuk TTL/eviction behavior |
| Kafka/RabbitMQ | baik tapi berat | berguna untuk consumer logic | real untuk ordering/redelivery |
| S3/object storage | emulator cukup | mock untuk unit | emulator untuk integration |
| Payment gateway | jangan real by default | stub/sandbox | contract penting |
| Email provider | mail catcher | stub | hindari 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.
| Symptom | Likely Cause | Fix |
|---|---|---|
| test kadang gagal koneksi DB | readiness salah | healthcheck + migration job |
| test gagal di CI saja | resource/CPU lambat | timeout realistis + readiness explicit |
| port already allocated | static port | no publish / dynamic port / unique project |
| data test tercampur | volume tidak dihapus | down -v, tmpfs |
| service tidak resolve | salah DNS/localhost | pakai service name |
| test order dependent | shared fixture mutable | isolate scenario data |
| queue test flaky | async wait salah | await condition, bukan sleep |
| E2E flaky | UI readiness salah | explicit wait pada user-visible state |
| image tidak update | stale 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 statssaat 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 Dependency | PR Pipeline | Nightly | Manual |
|---|---|---|---|
| unstable sandbox | stub | real | real |
| critical integration | contract + stub | real | real |
| costly API | stub | selective | selective |
| irreversible operation | fake only | sandbox with guard | manual 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?
| Kriteria | Compose | Testcontainers |
|---|---|---|
| Multi-service app graph | sangat baik | bisa, tapi kode test lebih kompleks |
| Per-test container lifecycle | kurang natural | sangat baik |
| Developer environment reuse | sangat baik | kurang sebagai dev stack |
| CI parity dengan app stack | baik | baik untuk integration tests |
| Dynamic ports | manual | built-in |
| Test code controls dependency | kurang | sangat baik |
| Cross-language standardization | baik | bergantung 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 upcukup untuk menjalankan test.
Lab 3 — Migration and Seed Jobs
Pisahkan:
migratejob;seedjob;apiservice;test-runner.
Kriteria:
apitidak 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
| Level | Indikator |
|---|---|
| Junior | bisa menjalankan DB dependency dengan Compose |
| Intermediate | punya healthcheck, test runner, dan cleanup script |
| Senior | mengelola 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
- Docker Docs — Docker Compose overview: https://docs.docker.com/compose/
- Docker Docs —
docker compose up: https://docs.docker.com/reference/cli/docker/compose/up/ - Docker Docs — Compose profiles: https://docs.docker.com/compose/how-tos/profiles/
- Docker Docs — Compose environment variables: https://docs.docker.com/compose/how-tos/environment-variables/envvars/
- Docker Docs — Continuous integration with Docker: https://docs.docker.com/build/ci/
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.
Keep the momentum while the lesson is still fresh. Move backward for review or continue forward into the next concept.