Test Pyramid Backend adalah strategi untuk menempatkan sebagian besar pengujian di level unit, lebih sedikit di level integrasi, dan paling sedikit di level end-to-end (E2E). Tujuannya sederhana: mendeteksi regresi secepat mungkin, menjaga feedback loop tetap singkat, dan menghindari pipeline CI yang mahal hanya karena terlalu banyak test lambat.
Masalah yang sering terjadi di tim backend bukan kurang test, tetapi salah distribusi test. Semua perilaku penting diuji lewat API E2E, hasilnya build lama, test flakey, dan debugging sulit. Sebaliknya, ada juga tim yang terlalu fokus pada unit test dengan mock berlebihan sampai bug nyata di database, query, transaction, atau integrasi antarlayer lolos ke production. Test pyramid membantu menyeimbangkan semuanya.
Apa yang dimaksud dengan test pyramid di backend
Dalam konteks backend, test pyramid berarti membagi pengujian berdasarkan biaya eksekusi, kecepatan feedback, dan cakupan risiko.
- Unit test: cepat, murah, fokus pada logika kecil dan deterministik.
- Integration test: sedang, memverifikasi interaksi nyata antar komponen seperti service ke repository, repository ke database, atau handler ke storage.
- E2E test: paling mahal, memverifikasi alur bisnis penting dari pintu masuk sistem sampai output akhir.
Piramida ini bukan aturan matematis yang kaku. Tidak ada rasio universal yang selalu benar. Namun prinsipnya tetap sama: semakin mahal test, semakin selektif penggunaannya.
Pembagian tanggung jawab: unit, integrasi, dan E2E
1. Unit test: menguji logika, bukan infrastruktur
Unit test cocok untuk bagian yang memiliki aturan jelas dan dapat dievaluasi tanpa ketergantungan nyata ke jaringan, database, filesystem, atau message broker. Fokusnya adalah perilaku logika.
Contoh yang cocok untuk unit test:
- Validasi aturan domain, misalnya status order boleh berubah dari pending ke paid tetapi tidak langsung ke refunded.
- Perhitungan harga, diskon, pajak, atau biaya layanan.
- Pemetaan error internal menjadi response code tertentu.
- Service yang menggabungkan beberapa aturan bisnis sebelum memanggil repository.
Contoh pseudo-code unit test untuk service:
function test_create_order_rejects_empty_items() {
repository = new FakeOrderRepository()
paymentGateway = new FakePaymentGateway()
service = new OrderService(repository, paymentGateway)
error = service.createOrder({ customerId: "c1", items: [] })
assert(error.code == "EMPTY_ITEMS")
assert(repository.savedOrdersCount() == 0)
}Mengapa ini efektif? Karena test seperti ini:
- sangat cepat dijalankan,
- mudah di-debug,
- tepat untuk memverifikasi banyak variasi input dan edge case.
Catatan: Unit test tidak harus selalu memakai mock. Jika dependensinya bisa diganti dengan fake sederhana yang lebih realistis dan stabil, sering kali itu lebih baik daripada mock yang terlalu rapuh.
2. Integration test: menguji batas antar komponen nyata
Integration test digunakan ketika bug yang ingin dicegah muncul dari interaksi antarkomponen, bukan dari logika murni. Ini sangat relevan di backend, karena banyak masalah justru terjadi di area seperti:
- query database yang salah,
- mapping kolom atau serialisasi yang keliru,
- transaction tidak bekerja seperti yang diasumsikan,
- constraint database tidak tertangani dengan benar,
- integrasi repository-service tidak sesuai kontrak.
Contoh yang cocok untuk integration test:
- Repository menyimpan dan membaca data dari database nyata.
- Endpoint API memanggil service dan repository dengan dependency nyata kecuali layanan eksternal.
- Pengujian transaction rollback saat salah satu operasi gagal.
- Pengecekan migrasi dan query terhadap skema database aktual.
Contoh pseudo-code integration test untuk repository dan database:
function test_user_repository_saves_and_loads_user() {
db = TestDatabase.connect()
db.reset()
repository = new UserRepository(db)
repository.save({ id: "u1", email: "a@example.com", status: "active" })
user = repository.findByEmail("a@example.com")
assert(user.id == "u1")
assert(user.status == "active")
}Test ini lebih lambat daripada unit test, tetapi nilainya tinggi karena memverifikasi perilaku yang tidak bisa dijamin oleh mock. Kalau query salah, indeks tidak relevan, atau mapping field keliru, test ini akan menangkapnya.
3. E2E test: menguji alur kritis dari sudut pandang sistem
E2E test menguji sistem sebagai satu kesatuan. Di backend, E2E biasanya memanggil endpoint seperti klien nyata, melewati routing, middleware, validasi, service, repository, dan database, lalu memverifikasi hasil akhirnya.
E2E cocok untuk:
- alur bisnis utama seperti login, checkout, create order, cancel order, atau proses pembayaran internal,
- jalur yang melibatkan banyak komponen dan memiliki dampak bisnis tinggi,
- kontrak perilaku sistem yang sangat penting untuk dijaga saat refactor besar.
Contoh pseudo-code E2E untuk endpoint API:
function test_create_order_endpoint_returns_201_and_persists_order() {
app = TestApplication.start()
app.resetState()
response = app.http.post("/orders", {
customerId: "c1",
items: [{ sku: "sku-1", qty: 2 }]
})
assert(response.status == 201)
assert(response.body.orderId != null)
saved = app.db.queryOne("select * from orders where id = ?", [response.body.orderId])
assert(saved.customer_id == "c1")
}Karena E2E paling mahal dan paling rentan flakey, jumlahnya harus dijaga tetap kecil dan fokus pada happy path serta beberapa failure path yang paling kritis.
Contoh pemetaan test untuk endpoint API, service, repository, dan database
Ambil contoh endpoint POST /orders dengan alur berikut:
- Validasi payload.
- Cek stok.
- Hitung total.
- Simpan order dan item dalam transaction.
- Kirim event internal atau enqueue job.
- Kembalikan respons API.
Pemetaan test yang sehat bisa seperti ini:
Unit test
- Service menolak order dengan item kosong.
- Perhitungan total benar saat ada diskon atau pembulatan.
- Aturan domain stok tidak boleh negatif.
- Mapper error dari service ke kode domain konsisten.
Integration test
- Repository menyimpan order dan item dalam transaction yang sama.
- Rollback terjadi jika insert item gagal.
- Endpoint memvalidasi request dan menyimpan data dengan dependency nyata.
- Job/event tercatat di komponen outbox atau queue adapter test.
E2E test
- Happy path pembuatan order berhasil dan data benar-benar tersimpan.
- Order gagal saat stok tidak cukup dan sistem mengembalikan respons yang benar.
- Satu skenario otorisasi penting, misalnya user tanpa hak akses mendapat 403.
Perhatikan bahwa tidak semua kombinasi error perlu naik ke E2E. Jika aturan stok sudah diuji detail di unit/service, dan penyimpanan ke database sudah diuji di integration test, maka E2E cukup memverifikasi beberapa jalur sistem yang paling mewakili risiko bisnis.
Bagaimana memilih test yang masuk smoke test vs full suite
Salah satu penyebab CI boros adalah semua test dijalankan di semua tahap dengan prioritas yang sama. Lebih efisien jika test dibagi ke beberapa lapisan eksekusi.
Smoke test
Smoke test adalah subset kecil yang harus cepat dan cukup kuat untuk mendeteksi kegagalan besar. Biasanya dijalankan:
- di setiap pull request,
- sebelum merge ke branch utama,
- kadang juga sebelum deploy.
Isi smoke test backend yang umum:
- seluruh unit test yang cepat,
- integration test inti untuk database dan wiring dasar aplikasi,
- beberapa E2E jalur bisnis paling kritis.
Kriteria masuk smoke test:
- sering gagal ketika ada regresi penting,
- waktu eksekusinya masih masuk akal,
- mewakili komponen inti aplikasi,
- tidak terlalu flakey.
Full suite
Full suite dijalankan lebih jarang, misalnya setelah merge, terjadwal, atau sebelum rilis. Isinya mencakup:
- semua unit test,
- semua integration test,
- seluruh E2E yang memang dipertahankan,
- test regresi tambahan untuk kasus langka namun berisiko tinggi.
Pola praktisnya:
- PR pipeline: lint, static analysis, unit test, integration test inti, E2E smoke.
- Main branch: full suite.
- Nightly: full suite + skenario lebih berat seperti migrasi, kompatibilitas, atau test data lebih besar.
Teknik mencegah regresi tanpa menumpuk E2E
Banyak tim menambah E2E setiap kali bug lolos. Dalam jangka pendek terasa aman, tetapi lama-lama suite menjadi lambat dan sulit dirawat. Strategi yang lebih sehat adalah memilih level test berdasarkan sumber bug.
Naikkan test ke level terendah yang cukup
Jika bug berasal dari logika perhitungan, tambahkan unit test. Jika bug berasal dari query atau transaction, tambahkan integration test. Tambahkan E2E hanya jika kegagalan memang baru terlihat saat seluruh alur sistem berjalan bersama.
Aturan praktis:
- Bug logika murni → unit test.
- Bug kontrak antar komponen → integration test.
- Bug orchestration lintas layer → E2E.
Gunakan contract atau adapter test untuk dependency eksternal
Kalau backend bergantung pada layanan eksternal seperti payment gateway, email provider, atau service internal lain, jangan paksa semua validasi lewat E2E penuh. Lebih efektif membuat test di boundary adapter:
- request yang dibentuk benar,
- response dan error diparse benar,
- timeout atau retry diterjemahkan ke error domain yang tepat.
Dengan begitu Anda tidak perlu banyak E2E hanya untuk memverifikasi integrasi dasar ke layanan luar.
Pisahkan happy path dari kombinasi edge case
E2E sebaiknya menutup beberapa happy path utama dan beberapa failure path penting. Kombinasi edge case yang banyak lebih cocok diuji di unit atau integration test. Misalnya:
- 10 variasi diskon → unit test.
- 3 variasi constraint database → integration test.
- 1-2 alur checkout kritis → E2E.
Kurangi flakiness dengan kontrol state test
Test lambat sering bukan karena jumlahnya saja, tetapi karena setup yang tidak terisolasi. Beberapa praktik yang membantu:
- reset state database per test atau per suite dengan cara yang konsisten,
- hindari ketergantungan antar test,
- gunakan data seed minimum,
- bekukan waktu jika logika bergantung pada waktu,
- hindari ketergantungan ke service eksternal nyata dalam PR pipeline.
Anti-pattern umum pada strategi test backend
Terlalu banyak mock
Mock berguna untuk mengisolasi logika, tetapi jika hampir semua dependency dimock, test bisa lulus meskipun integrasi nyatanya rusak. Gejalanya:
- unit test banyak dan hijau, tetapi bug query atau konfigurasi sering lolos,
- test sulit dibaca karena fokus pada detail interaksi, bukan hasil bisnis,
- refactor kecil memecahkan banyak test karena mock terlalu ketat.
Gunakan mock untuk dependency yang benar-benar ingin dikontrol, terutama yang mahal atau nondeterministik. Untuk dependency internal yang sederhana, fake atau integration test sering lebih bernilai.
Terlalu bergantung pada test manual
Test manual masih berguna untuk eksplorasi, validasi UX, atau investigasi insiden. Namun untuk regresi backend, test manual tidak skalabel. Masalah utamanya:
- mudah terlewat,
- tidak konsisten,
- tidak memberi feedback cepat di CI,
- mahal diulang untuk setiap perubahan kecil.
Jika sebuah skenario penting berulang kali diuji manual sebelum rilis, itu tanda kuat bahwa skenario tersebut perlu diotomasi di level yang tepat.
Semua bug dibalas dengan E2E baru
Ini anti-pattern klasik. E2E memang terasa paling meyakinkan, tetapi biaya perawatannya tinggi. Jika setiap bug menambah satu E2E tanpa evaluasi level yang tepat, pipeline akan melambat dan tim mulai menoleransi build lama.
Integration test palsu yang masih penuh stub
Sering ada test yang disebut integration test, padahal database, queue, dan adapter utama masih diganti stub semua. Hasilnya tidak benar-benar menguji integrasi yang penting. Beri nama test sesuai realitasnya, supaya ekspektasi tim tidak salah.
Matriks keputusan: kapan memakai jenis test apa
| Kasus | Jenis test utama | Alasan |
|---|---|---|
| Perhitungan diskon dan total | Unit test | Logika deterministik, banyak variasi input, harus cepat |
| Validasi transisi status order | Unit test | Aturan domain murni, tidak butuh database |
| Query pencarian user berdasarkan email | Integration test | Perlu verifikasi query, mapping, dan skema nyata |
| Transaction order + order item | Integration test | Perilaku rollback tidak bisa dipercaya dari mock |
| Endpoint create order happy path | E2E test | Mewakili alur bisnis inti lintas layer |
| Endpoint unauthorized access | E2E atau integration test | Tergantung apakah ingin menguji middleware/wiring penuh |
| Adapter ke payment provider | Integration/contract test | Fokus pada boundary eksternal, bukan seluruh sistem |
| Retry logic saat timeout layanan luar | Unit + integration test | Logika retry di unit, boundary adapter di integration |
Gunakan matriks ini sebagai panduan awal, bukan aturan absolut. Jika satu skenario sangat berisiko bisnis, masuk akal punya lebih dari satu lapisan test untuk kasus tersebut.
Workflow verifikasi di local dan CI
Workflow local untuk developer
Developer membutuhkan feedback paling cepat di mesin lokal. Workflow yang praktis:
- Jalankan unit test terkait file yang diubah.
- Jalankan integration test untuk modul yang terdampak, terutama jika ada perubahan query, skema, transaction, atau wiring.
- Jalankan E2E smoke hanya jika perubahan menyentuh alur API utama, auth, atau konfigurasi aplikasi.
Tujuannya bukan meniru seluruh CI di local, tetapi menangkap error yang paling mungkin sebelum push.
Contoh urutan perintah secara generik:
# 1. cek cepat saat develop
run-unit-tests --changed
# 2. jika ada perubahan layer data / endpoint
run-integration-tests --module orders
# 3. sebelum membuka PR untuk perubahan kritis
run-e2e-tests --smokeWorkflow CI yang efisien
Pipeline CI sebaiknya dibagi menjadi tahap yang jelas, bukan satu langkah besar.
- Fast checks: formatting, lint, static analysis, dan unit test cepat.
- Core integration: integration test inti yang memakai database atau komponen penting lain.
- Smoke E2E: sedikit skenario end-to-end yang mewakili kesehatan sistem.
- Full suite: dijalankan setelah merge atau terjadwal.
Prinsip penting untuk CI:
- paralelkan suite yang independen,
- pisahkan test lambat dari test cepat,
- pastikan hasil failure mudah ditelusuri ke level yang tepat,
- hindari menjalankan full E2E pada setiap perubahan kecil jika nilainya rendah.
Praktik yang sering efektif: jalankan semua unit test pada setiap PR, tetapi batasi E2E ke smoke set yang benar-benar kritis. Full E2E lebih cocok setelah merge ke branch utama atau sebelum rilis.
Checklist implementasi bertahap untuk tim kecil
Tim kecil sering tidak punya kapasitas membangun strategi testing ideal sekaligus. Pendekatan bertahap biasanya lebih realistis.
Tahap 1: petakan area risiko
- Identifikasi 3-5 alur backend paling kritis secara bisnis.
- Identifikasi bug yang paling sering lolos: logika, query, auth, transaction, atau konfigurasi.
- Tentukan target awal smoke test.
Tahap 2: perkuat unit test untuk domain logic
- Pindahkan aturan bisnis dari controller/handler ke service atau domain layer agar mudah diuji.
- Tambahkan unit test untuk validasi, kalkulasi, dan transisi state.
- Kurangi ketergantungan ke mock yang tidak perlu.
Tahap 3: tambahkan integration test di titik rawan
- Repository yang berinteraksi dengan database nyata.
- Transaction penting.
- Endpoint yang sering rusak karena wiring atau validasi.
Tahap 4: pilih sedikit E2E yang bernilai tinggi
- Login atau autentikasi utama.
- Create/read/update flow yang paling kritis.
- Satu atau dua jalur error yang dampaknya besar.
Tahap 5: bentuk pembagian suite CI
- PR: unit + integration inti + smoke E2E.
- Main/nightly: full suite.
- Evaluasi test yang lambat, flakey, atau duplikatif.
Tahap 6: setiap bug baru tambahkan test di level paling tepat
- Jangan otomatis menambah E2E.
- Tanyakan: bug ini seharusnya bisa ditangkap di level mana dengan biaya paling rendah?
Tips debugging saat suite mulai lambat atau tidak stabil
Jika integration test lambat
- Periksa apakah setup database diulang terlalu mahal per test.
- Kurangi fixture besar yang tidak diperlukan.
- Kelompokkan test berdasarkan modul agar setup bisa dipakai ulang secara aman.
- Pastikan query atau migrasi test tidak melakukan kerja berlebihan.
Jika E2E flakey
- Cek race condition pada background job, event, atau commit transaction.
- Hindari sleep statis; lebih baik tunggu kondisi yang benar-benar relevan.
- Pastikan state awal selalu bersih.
- Bekukan atau kontrol waktu jika timestamp memengaruhi hasil.
Jika unit test sulit dipelihara
- Kurangi assertion terhadap detail implementasi internal.
- Fokus pada input-output dan aturan bisnis.
- Pertimbangkan fake object daripada mock yang mengunci urutan call secara rapuh.
Penutup
Test Pyramid Backend bukan sekadar membagi test menjadi tiga label, tetapi cara berpikir untuk menaruh verifikasi di level yang paling murah namun tetap efektif. Unit test menjaga logika tetap aman dan cepat, integration test memastikan komponen benar-benar bekerja bersama, dan E2E memverifikasi alur bisnis inti tanpa harus menjadi tempat semua regresi diuji.
Jika target Anda adalah coverage yang tinggi tanpa CI lambat, kuncinya bukan menambah semua jenis test sekaligus, melainkan memilih tanggung jawab yang tepat untuk setiap level. Mulailah dari area yang paling berisiko, bentuk smoke suite yang ramping, dan tambahkan test baru berdasarkan sumber bug nyata. Dengan begitu, pipeline tetap sehat dan kepercayaan terhadap perubahan kode tetap tinggi.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!