Chapter Debugging Container berakhir pada kesadaran yang tidak nyaman: seluruh kejelian logs, exec, dan inspect tetap datang setelah kegagalan terjadi — kita memburu galat yang sudah berumur dan menambal kode yang sudah terlanjur salah. Test adalah cara bergerak sebelum kegagalan lahir, dan container membuatnya lebih kuat dari yang terlihat: test yang berjalan di dalam image yang sama dengan produksi tidak lagi bisa dielusi dengan kalimat “di mesin saya jalannya baik-baik saja”, karena mesin tempatnya berjalan adalah image itu sendiri. Manfaat kedua menunggu di ujung jalan — pipeline CI kelak menjalankan perintah yang persis sama pada image yang persis sama, sehingga apa pun yang kita yakini di laptop akan tetap benar di server build. Chapter ini membangun dua lapis test untuk katalog-web: unit test lewat docker compose run, lalu integration test dengan PostgreSQL sekali pakai yang hidup di atas tmpfs.
Unit Test Tanpa Meninggalkan Workflow #
Unit test menguji fungsi murni tanpa menyentuh jaringan atau database, dan katalog-web kini punya subjek uji yang layak: Budi mengekstrak fungsi format harga ke modul src/rupiah.js agar bisa diuji terpisah dari server. Berkas test/unit/harga.test.js memakai node --test, test runner bawaan Node.js 22 yang tidak menuntuh satu dependensi eksternal pun:
const { test } = require("node:test");
const assert = require("node:assert");
const { formatRupiah } = require("../../src/rupiah");
test("memformat harga ke rupiah penuh", () => {
assert.equal(formatRupiah(85000), "Rp85.000");
});
test("menolak harga negatif", () => {
assert.throws(() => formatRupiah(-1000), /harga tidak boleh negatif/);
});Skrip npm test sudah tertanam pada manifest sejak Chapter Development dan Production Image, dan menjalankannya dalam container hanya menuntut satu perintah yang kita kenal dari Chapter Menjalankan dan Mengelola Stack:
$ docker compose run --rm web npm testOutput yang Dihasilkan:
> [email protected] test
> node --test test/unit/
▶ test/unit/harga.test.js (2)
✔ memformat harga ke rupiah penuh
✔ menolak harga negatif
└─ pass 2 - fail 0
ℹ tests 2
ℹ suites 1
ℹ pass 2
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 387Mari kita telaah mengapa run — bukan exec — adalah kendaraan yang tepat untuk test:
runmenciptakan container baru dari cetakbiru serviceweb. Environment darienv_filedanenvironment, network project, bind mount source code, serta volumenode_modulesdari Chapter Development Dependencies semuanya diterapkan — test melihat kode dan dependensi persis seperti yang dilihat aplikasi yang sedang berjalan.execakan menempel ke container hidup yang sama. Keadaan container lama belum tentu mencerminkan hasil build terbaru, dan proses test yang berat mengganggu aplikasi yang sedang melayani permintaan;runmenjaga keduanya bersih.- Port host tidak diterbitkan pada
run. Perilaku yang kita catat sebagai pengecualian penting pada Chapter Menjalankan dan Mengelola Stack justru menguntungkan di sini — test tidak memerlukan pintu host, dan dua sesi test paralel tidak akan saling merebut port. --rmmembuang bekasnya seketika. Tanpa flag ini, containerkatalog-web-run-...berstatusExitedmenumpuk didocker compose ps -a.--no-depstersedia bila stack sedang mati. Secara bawaanrunjuga menyalakan dependensi service —dbakan dibangunkan dan menunggu gerbangservice_healthylebih dulu; untuk unit test yang tidak menyentuh database,docker compose run --rm --no-deps web npm testmelewati seluruh upacara itu.
| Aspek | docker compose run | docker compose exec |
|---|---|---|
| Container yang dipakai | baru, diciptakan dari cetakbiru service | yang sedang berjalan |
| Isi yang terlihat | hasil build dan suntingan termutakhir | keadaan container lama |
| Port host | tidak diterbitkan | milik container yang hidup |
| Cocok untuk | tugas sekali jalan, test, migrasi | intip cepat pada container hidup |
Exit Code sebagai Kontrak #
Sesi test yang selalu hijau belum membuktikan apa-apa sampai kita menyaksikan kegagalannya bekerja. Budi merefactor formatRupiah agar memakai toLocaleString, dan satu ekspektasi tidak lagi terpenuhi:
$ docker compose run --rm web npm testOutput yang Dihasilkan (dipotong):
> [email protected] test
> node --test test/unit/
▶ test/unit/harga.test.js (2)
✔ menolak harga negatif
✖ memformat harga ke rupiah penuh
---
error: 'Rp85.00' == 'Rp85.000'
operator: '=='
expected: 'Rp85.000'
actual: 'Rp85.00'
...
└─ fail 1 - pass 1
ℹ tests 2
ℹ suites 1
ℹ pass 1
ℹ fail 1
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 419Perintah berikutnya hanya satu karakter, tetapi itulah seluruh kontraknya:
$ echo $?Output yang Dihasilkan:
1Mari kita telaah rantai tanggung jawab di balik angka 1 tersebut:
node --testkeluar dengan kode 1 karena ada test gagal. Test runner bertindak sebagai wasit yang menerjemahkan “dua ekspektasi, satu pecah” menjadi satu angka.npm testmeneruskan kode itu. npm tidak menelan kegagalan skrip — kode keluar skrip menjadi kode keluar npm.docker compose runmeneruskannya ke shell host. CLI menunggu container selesai lalu menutup dirinya dengan kode yang sama, persis perilakudocker runmode terpasang yang kita bedah pada Chapter Memahami Exit Code —$?di host membaca kegagalan yang lahir di dalam container.- Inilah titik tangan mesin mengambil alih. Pipeline CI kelak tidak membaca tulisan merah di layar; ia membaca angka ini dan memutuskan build lulus atau gugur — materi Section CI/CD dan Automation.
Integration Test dengan Database Sekali Pakai #
Integration test menguji hal yang tidak bisa dijawab unit test: apakah aplikasi sungguhan sanggup bicara dengan PostgreSQL sungguhan. Tuntutannya ada tiga — database yang hidup, data yang selalu mulai bersih, dan kecepatan yang membuatnya layak dijalankan berkali-kali sehari. Compose menjawab ketiganya lewat satu profile bernama test dengan dua service tambahan pada compose.yaml:
db-test:
image: postgres:18-alpine
environment:
POSTGRES_USER: katalog
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: katalog
tmpfs:
- /var/lib/postgresql/data
healthcheck:
test: ["CMD-SHELL", "pg_isready -U katalog -d katalog"]
interval: 2s
timeout: 3s
retries: 15
profiles: ["test"]
test:
build:
context: .
target: dev
image: katalog-web:dev
environment:
DATABASE_URL: postgres://katalog:${POSTGRES_PASSWORD}@db-test:5432/katalog
depends_on:
db-test:
condition: service_healthy
command: npm run test:integration
profiles: ["test"]Manifest menambahkan satu skrip, dan folder test/integration/ mendapat penghuninya — test yang membuat skemanya sendiri sebelum menguji:
{
"scripts": {
"dev": "nodemon src/server.js",
"test": "node --test test/unit/",
"test:integration": "node --test test/integration/"
}
}const { test, before, after } = require("node:test");
const assert = require("node:assert");
const { Pool } = require("pg");
const pool = new Pool({ connectionString: process.env.DATABASE_URL });
before(async () => {
await pool.query(`CREATE TABLE IF NOT EXISTS produk (
id SERIAL PRIMARY KEY, nama TEXT NOT NULL, harga INTEGER NOT NULL)`);
await pool.query("TRUNCATE produk RESTART IDENTITY");
await pool.query("INSERT INTO produk (nama, harga) VALUES ('Kopi Arabika Gayo', 85000)");
console.log("migrasi uji: tabel produk siap di db-test");
});
test("daftar produk terbaca dari database", async () => {
const hasil = await pool.query("SELECT nama, harga FROM produk ORDER BY id");
assert.equal(hasil.rows.length, 1);
assert.equal(hasil.rows[0].nama, "Kopi Arabika Gayo");
assert.equal(hasil.rows[0].harga, 85000);
});
after(async () => {
await pool.end();
});Sesi integration test dinyalakan dengan satu perintah yang menyebut profilnya secara eksplisit:
$ docker compose --profile test run --rm testOutput yang Dihasilkan:
[+] Running 1/1
✔ Container katalog-db-test-1 Healthy
> [email protected] test:integration
> node --test test/integration/
ℹ migrasi uji: tabel produk siap di db-test
▶ test/integration/produk.test.js (1)
✔ daftar produk terbaca dari database
└─ pass 1 - fail 0
ℹ tests 1
ℹ suites 1
ℹ pass 1
ℹ fail 0
ℹ cancelled 0
ℹ skipped 0
ℹ todo 0
ℹ duration_ms 1846Mari kita telaah lima keputusan yang membuat rangkaian ini bekerja:
tmpfspada/var/lib/postgresql/datamenghapus disk dari pemandangan. Cluster database uji hidup sepenuhnya di memori — tanpa volume, tanpa tulis disk, tanpa satu megabita pun yang menetap setelah sesi berakhir; inilah use case cache dan data fana yang kita petakan pada Chapter tmpfs Mount, kini dipakai untuk kecepatan murni:initdbdi atas RAM selesai dalam hitungan detik.- Reset antar sesi dikerjakan oleh siklus hidup container, bukan skrip pembersih.
runmenciptakan containertestbaru setiap dipanggil, dan begitudb-testdihentikan lalu dibuat ulang — atau dijalankan ulang setelahdown— tmpfs-nya terlahir kosong kembali sehingga entry point postgres menjalankaninitdbdari awal. Database uji selalu mulai dari nol tanpa satu perintahDELETEpun. - Healthcheck sengaja lebih rapat dari kebiasaan stack utama.
interval: 2sdenganretries: 15memberi gerbang yang tanggap — probepg_isreadymemeriksa setiap dua detik dengan anggaran kesabaran 30 detik untuk mesin lambat — pola yang kita bangun pada Chapter Healthcheck dan Service Readiness, dikencangkan karena sesi uji tidak boleh menghangur menunggu. DATABASE_URLmenunjukdb-test, bukandb. Nama service yang berbeda membuat jalur uji sepenuhnya terpisah dari jalur development; volumekatalog_db-datatempat data kerja kita tidak pernah tersentuh olehTRUNCATEmilik berkas uji.profiles: ["test"]menjaga keduanya tidur pada hari biasa.docker compose up -dtidak pernah menyalakan pasangan ini; kebetulan menyebut nama servicetestsecara langsung pun mengaktifkan profilnya otomatis — perilaku yang kita buktikan pada Chapter Profiles — sehingga perintah eksplisit di atas sebenarnya boleh dipersingkat menjadidocker compose run --rm test.
Sebuah pembedaan perlu ditegaskan agar dua wajah pengujian tidak tertukar. Stage test pada Dockerfile dari Chapter Development dan Production Image menjalankan unit test saat build sebagai gerbang kelahiran image produksi; service test pada compose menjalankan integration test sebagai container hidup dengan database sungguhan. Keduanya saling melengkapi: gerbang build menolak image yang cacat, sesi integration menolak asumsi yang cacat. Satu kebiasaan terakhir dari pola run yang perlu dibawa: perintah ini tidak membangun ulang image — bila berkas test di host lebih baru daripada image katalog-web:dev, jalankan docker compose build test lebih dulu, kebiasaan --build yang kita paku sejak Chapter Image, Build, dan Pull.
Sesi uji ditutup dengan membenahi korselnya: docker compose rm -sf db-test menghentikan dan membuang container database uji tanpa menyentuh stack utama, dan karena tmpfs tidak meninggalkan volume, tidak ada satu objek data pun yang perlu dibersihkan sesudahnya.
Peta Jenis Test dan Wadahnya #
Dua lapis yang kita bangun hari ini menempati dua baris pertama dari peta yang lebih besar:
| Jenis test | Yang dibuktikan | Kebutuhan container | Perintah khas |
|---|---|---|---|
| Unit test | fungsi murni berperilaku benar | image dev saja, tanpa service lain | docker compose run --rm web npm test |
| Integration test | aplikasi dan database bekerjasama | image dev + db-test di atas tmpfs | docker compose --profile test run --rm test |
| End-to-end | seluruh stack melayani dari pintu proxy | stack penuh + skenario HTTP dari luar | curl terhadap stack staging — Chapter Mengelola Environment Development |
Baris ketiga sengaja ditinggalkan sebagai jembatan: end-to-end test membutuhkan environment yang menyerupai produksi — image dari registry, port 80, data yang bukan milik siapa pun — dan environment seperti itu harus dikelola dengan kesengajaan tersendiri. Manfaat terbesarnya baru terasa ketika laptop Budi, laptop Siti, dan mesin CI menjalankan kombinasi yang berbeda dari perangkat yang sama: profile yang berbeda, berkas lokal yang berbeda, port yang berbeda, tag image yang berbeda. Merapikan seluruh kombinasi itu menjadi peta yang bisa dibaca seluruh tim adalah pekerjaan chapter penutup section ini.
Referensi #
- docker compose run
— container sekali jalan dari cetakbiru service, flag
--rmdan--no-deps. - Compose profiles — aktivasi profil untuk service uji beserta aturan dependensinya.
- tmpfs Mounts — data sementara di memori untuk database sekali pakai.
- services — Compose file reference
— kunci
tmpfs,healthcheck, danprofilespada service.