Test Pyramid Backend adalah pendekatan untuk menata jenis pengujian agar mayoritas bug terdeteksi cepat di lapisan bawah, sementara test yang mahal dan lambat hanya dipakai seperlunya. Tujuannya bukan mengejar jumlah test terbanyak, melainkan feedback yang cepat, stabil, dan relevan terhadap risiko perubahan.
Pada praktik backend, masalah umumnya bukan kekurangan test, tetapi komposisi suite yang salah: terlalu banyak E2E, terlalu sedikit unit test yang benar-benar memeriksa aturan bisnis, integrasi yang menabrak database untuk semua kasus, dan pipeline CI yang makin lambat tiap sprint. Artikel ini membahas cara menyusun ulang test unit, integrasi, contract, dan E2E agar deteksi regresi tetap kuat tanpa boros waktu komputasi dan biaya CI.
Apa itu test pyramid untuk backend
Inti dari test pyramid adalah menaruh lebih banyak test pada lapisan yang murah, cepat, dan deterministik; lalu semakin sedikit test pada lapisan yang lebih berat. Pada backend, lapisan itu biasanya terlihat seperti ini:
- Unit test: menguji fungsi, class, domain service, validator, mapper, pricing rule, policy, dan logika murni tanpa ketergantungan eksternal nyata.
- Integration test: menguji interaksi dengan database, cache, message broker, filesystem, atau service internal lain.
- Contract test: memverifikasi kesepakatan antarsistem, misalnya format request/response API, event schema, atau perilaku minimum antara provider dan consumer.
- E2E test: menguji alur dari permukaan sistem sampai ke dependency penting, biasanya lewat HTTP API atau workflow bisnis utama.
Piramida ini bukan aturan matematis kaku. Yang penting adalah semakin tinggi level test, semakin selektif penggunaannya. Jika semua skenario dimasukkan ke E2E, CI akan lambat, flaky, sulit didiagnosis, dan mahal dirawat.
Peran tiap jenis test dan kapan dipakai
1. Unit test: garis pertahanan pertama
Unit test paling cocok untuk logika yang sering berubah dan mudah rusak jika hanya diuji lewat endpoint. Contohnya:
- perhitungan biaya, diskon, pajak, atau penalty,
- validasi aturan domain yang kompleks,
- normalisasi data, parser, mapper, serializer,
- otorisasi berbasis rule,
- state transition pada entitas domain.
Unit test sebaiknya sangat cepat, tidak bergantung pada database, jaringan, waktu sistem nyata, atau antrian nyata. Gunakan fake, stub, atau mock seperlunya untuk dependency yang bukan bagian dari logika yang sedang diuji.
Gunakan unit test ketika pertanyaan utamanya adalah: “Apakah aturan bisnis ini benar untuk kombinasi input tertentu?”
Contoh sederhana logika domain:
function calculateShippingFee(orderTotal, destinationZone, isPriority) {
let base = destinationZone === 'remote' ? 30000 : 12000;
if (orderTotal >= 500000) {
base = Math.max(0, base - 10000);
}
if (isPriority) {
base += 15000;
}
return base;
}
// contoh kasus yang perlu diuji:
// - zona normal, non-priority
// - zona remote, non-priority
// - diskon ongkir saat order total melewati threshold
// - priority menambah biaya setelah diskon dasar dihitung
Jika aturan seperti ini hanya diuji lewat endpoint checkout, kegagalan akan lebih sulit dilokalisasi. Unit test membuat masalah langsung terlihat pada level logika.
2. Integration test: verifikasi interaksi yang nyata
Integration test dibutuhkan ketika bug yang sering muncul justru ada di batas antara kode dan dependency, misalnya:
- query database salah atau tidak sesuai transaksi,
- constraint unik tidak tertangani,
- mapping ORM ke tabel bermasalah,
- serialisasi event ke broker tidak sesuai,
- caching menimbulkan data basi,
- job queue tidak memproses payload seperti yang diharapkan.
Untuk backend, integration test biasanya paling bernilai saat menyentuh:
- repository atau data access layer,
- migration penting,
- transaction boundary,
- integrasi Redis/cache,
- publisher/consumer event,
- adapter ke service internal.
Gunakan integration test ketika Anda memang perlu membuktikan bahwa sistem bekerja dengan dependency riil atau lingkungan yang cukup mirip dengan produksi. Tetapi hindari menjadikannya default untuk semua skenario. Jika logika dapat dibuktikan di unit test, jangan pindahkan semuanya ke database hanya demi rasa aman.
3. Contract test: mencegah integrasi antarservice rusak diam-diam
Pada arsitektur backend yang berinteraksi dengan service lain, contract test sering lebih efisien daripada E2E penuh. Contract test memeriksa bahwa provider dan consumer tetap sepakat tentang:
- bentuk payload JSON,
- field wajib dan opsional,
- tipe data,
- status code atau error envelope,
- schema event pada broker,
- perilaku minimum yang dijanjikan.
Ini penting karena banyak bug produksi bukan berasal dari logika internal, melainkan perubahan kontrak yang tidak terkoordinasi. Tanpa contract test, tim biasanya mengandalkan E2E lintas service yang mahal dan rapuh.
Gunakan contract test ketika pertanyaan utamanya adalah: “Apakah service A masih berbicara dengan service B sesuai kesepakatan?”
Contract test tidak menggantikan integration test internal dan juga tidak menggantikan E2E. Ia menutup celah spesifik: kerusakan di titik antarmuka antarservice.
4. E2E test: sedikit, strategis, dan fokus pada alur kritikal
E2E backend cocok untuk memverifikasi alur yang benar-benar penting dari sudut bisnis dan operasional, misalnya:
- registrasi pengguna sampai aktivasi,
- membuat order lalu pembayaran berhasil,
- refund diproses dan status tercermin benar,
- permission penting benar-benar menolak akses sensitif,
- alur idempoten untuk endpoint yang rawan dipanggil ulang.
E2E tidak perlu memeriksa semua cabang validasi kecil. Itu pekerjaan unit dan integrasi. E2E sebaiknya menjawab pertanyaan: “Apakah alur utama masih hidup dari sudut pandang konsumen sistem?”
Kesalahan yang sering terjadi adalah memakai E2E untuk memverifikasi terlalu banyak detail internal. Akibatnya, satu perubahan kecil pada pesan error, urutan field, atau data seed membuat banyak test gagal walaupun perilaku bisnis utama tetap benar.
Porsi test yang realistis untuk tim kecil-menengah
Tidak ada rasio universal, tetapi untuk banyak tim backend kecil-menengah, komposisi berikut cukup sehat sebagai titik awal:
- 60-75% unit test,
- 15-25% integration test,
- 5-10% contract test,
- 3-8% E2E test.
Angka ini bukan target mutlak, tetapi panduan agar suite tidak terbalik menjadi “ice cream cone”, yaitu sedikit unit test dan terlalu banyak test berat di atas.
Beberapa penyesuaian yang masuk akal:
- Jika domain bisnis sangat kompleks dan dependency eksternal sedikit, porsi unit test bisa lebih tinggi.
- Jika sistem sangat bergantung pada database, queue, dan cache, integration test mungkin perlu lebih banyak.
- Jika arsitektur mikroservice atau event-driven, contract test biasanya lebih penting.
- Jika produk punya sedikit workflow tetapi dampaknya sangat kritikal, E2E bisa sedikit lebih banyak, tetap dengan seleksi ketat.
Contoh struktur suite test backend
Struktur suite yang rapi membantu tim memahami tujuan tiap lapisan. Contoh umum:
tests/
unit/
domain/
pricing/
policy/
validation/
application/
use_cases/
services/
integration/
database/
repositories/
migrations/
cache/
queue/
external_adapters/
contract/
http/
provider/
consumer/
events/
e2e/
auth/
orders/
billing/
Pemisahan ini penting agar orang tidak mencampur unit test dengan integration test hanya karena semuanya lewat framework test yang sama. Jika sebuah test menyentuh database sungguhan, ia bukan unit test walaupun file-nya kecil.
Contoh klasifikasi kasus
- Unit: aturan diskon order berdasarkan kategori pelanggan dan nominal transaksi.
- Integration: repository menyimpan order beserta item dalam satu transaksi dan rollback saat item invalid.
- Contract: service payment provider masih mengembalikan field status, reference_id, dan error code sesuai kontrak.
- E2E: endpoint create-order sampai emit event order.created dan status order dapat diambil kembali.
Tanda coverage yang menipu
Code coverage berguna sebagai indikator kasar, tetapi sangat mudah menipu jika diperlakukan sebagai tujuan utama. Coverage tinggi tidak otomatis berarti risiko regresi rendah.
Beberapa tanda coverage yang menipu:
- Banyak assertion dangkal: test hanya memeriksa status 200 atau object tidak null, tanpa memverifikasi aturan bisnis penting.
- Coverage tinggi dari jalur happy path saja: edge case, error handling, retry, timeout, idempotency, dan transaction failure tidak tersentuh.
- Logic diuji tidak langsung lewat E2E: baris kode terhitung covered, tetapi sumber bug sulit ditemukan saat gagal.
- Terlalu banyak mocking: test lulus karena semua dependency dipaksa berperilaku ideal, padahal integrasi nyatanya rusak.
- Setter/getter atau kode boilerplate mendominasi coverage: persentase naik, kepercayaan tidak.
- Assertion pada detail implementasi: coverage ada, tetapi refactor kecil merusak test walau perilaku tetap benar.
Lebih baik punya coverage lebih rendah tetapi menutup area berisiko tinggi, daripada coverage tinggi yang sebagian besar tidak memberi sinyal saat terjadi regresi penting.
Penyebab suite lambat dan flaky
Penyebab suite lambat
- Terlalu banyak test menyentuh database untuk kasus yang sebenarnya bisa diuji sebagai unit.
- Setup data besar dan berulang di setiap test.
- Migration atau bootstrap environment dijalankan terlalu sering.
- Penggunaan jaringan nyata ke service eksternal pada jalur PR.
- Menjalankan seluruh suite untuk setiap perubahan kecil tanpa seleksi risiko.
- Serial execution padahal test aman dijalankan paralel.
- Fixture berat, seed besar, atau object graph yang tidak relevan dengan skenario.
Penyebab flaky test
- Ketergantungan pada waktu nyata, timezone, atau jam sistem.
- Asumsi urutan eksekusi test.
- State bersama di database, cache, file, atau environment variable.
- Race condition pada queue, event, atau proses async.
- Port dinamis, dependency container belum siap, atau startup belum stabil.
- Assertion terlalu ketat pada data nondeterministik seperti timestamp, UUID, atau urutan array yang tidak dijamin.
- Test bergantung pada API pihak ketiga yang responsnya tidak stabil.
Tips mengurangi flaky
- Bekukan waktu dengan clock abstraction atau test helper.
- Isolasi state per test dan bersihkan resource dengan disiplin.
- Gunakan retry hanya untuk infrastruktur yang benar-benar intermittent, bukan untuk menutupi bug logika.
- Untuk proses async, tunggu kondisi yang benar, bukan sleep tetap.
- Pastikan data seed minimal dan deterministik.
Memilih test berdasarkan risiko perubahan
Strategi yang sehat bukan “jalankan semua test untuk semua hal” pada setiap langkah. Pilih test berdasarkan risiko perubahan dan biaya kegagalan.
Gunakan tiga pertanyaan ini
- Apa dampak bisnis jika ini rusak? Misalnya checkout, payment, auth, permission, invoicing, dan data mutation biasanya berisiko tinggi.
- Seberapa sering area ini berubah? Area yang sering disentuh dan sering regress cocok diperkaya unit/integration test.
- Seberapa sulit bug ini dideteksi tanpa test lapisan tertentu? Misalnya masalah query dan transaction sulit dibuktikan tanpa integration test.
Matriks sederhana:
- Risiko tinggi + perubahan sering: unit test kuat, integration test penting, tambah contract/E2E jika menyangkut antarsistem atau alur kritikal.
- Risiko tinggi + perubahan jarang: simpan E2E atau integration test strategis untuk menjaga area sensitif.
- Risiko rendah + perubahan sering: utamakan unit test cepat agar PR tetap lincah.
- Risiko rendah + perubahan jarang: jangan over-test. Validasi secukupnya.
Contoh keputusan:
- Mengubah rumus fee: prioritaskan unit test untuk seluruh cabang logika, lalu satu integration atau E2E jika hasilnya memengaruhi endpoint utama.
- Mengubah repository dan migration: prioritaskan integration test database, bukan menambah banyak E2E.
- Mengubah payload event order.created: wajib contract test untuk producer/consumer terkait.
- Mengubah middleware auth: kombinasi unit test policy, integration test auth layer, dan beberapa E2E untuk jalur akses paling kritikal.
Workflow verifikasi di pull request dan sebelum rilis
Workflow di pull request
Tujuan PR check adalah memberi feedback cepat dan cukup untuk menahan regresi umum. Untuk tim kecil-menengah, workflow ini sering efektif:
- Jalankan linting dan static analysis.
- Jalankan seluruh unit test.
- Jalankan integration test terpilih untuk area yang berubah atau area berisiko tinggi.
- Jalankan contract test yang terkait perubahan antarmuka.
- Jalankan subset E2E smoke test yang singkat, misalnya auth, create resource, dan satu alur transaksi inti.
PR bukan tempat terbaik untuk seluruh E2E panjang kecuali sistemnya kecil. Jika semua E2E diwajibkan di setiap PR, feedback loop akan memburuk dan developer mulai menganggap test sebagai penghambat, bukan alat kontrol kualitas.
Workflow sebelum rilis
Sebelum rilis atau deploy ke environment yang lebih tinggi:
- Jalankan suite PR lengkap.
- Tambahkan full integration suite jika sebelumnya hanya subset.
- Jalankan full contract verification untuk service yang akan dirilis.
- Jalankan E2E regresi kritikal untuk alur bisnis utama dan incident yang pernah terjadi sebelumnya.
- Jika perlu, jalankan verifikasi smoke di environment staging dengan konfigurasi semirip mungkin dengan produksi.
Pisahkan tujuan tiap tahap. PR untuk feedback cepat. Pre-release untuk keyakinan lebih tinggi. Produksi untuk monitoring, alerting, dan rollback plan. Test bukan satu-satunya pagar pengaman.
Contoh kebijakan eksekusi suite di CI
# Konsep pipeline, bukan format tool tertentu
stages:
- lint
- unit
- integration_changed
- contract_changed
- e2e_smoke
- pre_release_full
pull_request:
run:
- lint
- all unit tests
- integration tests for changed modules
- contract tests for affected APIs/events
- smoke e2e for critical flows
main_branch_or_release:
run:
- all unit tests
- full integration tests
- full contract tests
- critical e2e regression suite
Intinya bukan tool CI tertentu, melainkan disiplin membedakan fast feedback path dan high confidence path.
Checklist audit test stack backend
Gunakan checklist ini untuk menilai apakah suite Anda sehat atau mulai boros:
Komposisi dan tujuan
- Apakah mayoritas test ada di lapisan unit?
- Apakah setiap E2E punya alasan bisnis yang jelas?
- Apakah contract test sudah ada untuk API atau event yang dikonsumsi service lain?
- Apakah integration test fokus pada batas sistem, bukan menggantikan unit test?
Kecepatan dan stabilitas
- Apakah PR check selesai cukup cepat untuk dipakai rutin oleh developer?
- Apakah flaky test tercatat, diinvestigasi, dan tidak dibiarkan menumpuk?
- Apakah setup test memakai fixture minimal?
- Apakah ada test yang bergantung pada jaringan eksternal di jalur PR?
Kualitas sinyal
- Apakah test gagal memberi pesan yang jelas tentang penyebabnya?
- Apakah assertion memeriksa perilaku penting, bukan hanya status umum?
- Apakah incident produksi sebelumnya sudah diterjemahkan menjadi test permanen?
- Apakah area risiko tinggi benar-benar punya coverage bermakna?
Perawatan jangka panjang
- Apakah test mudah dibaca dan diubah saat refactor?
- Apakah helper test mengurangi duplikasi tanpa menyembunyikan intent?
- Apakah data test deterministik dan tidak saling bocor?
- Apakah tim sepakat soal definisi unit, integrasi, contract, dan E2E?
Langkah bertahap memperbaiki test stack yang sudah terlanjur berat
Jika suite Anda sudah penuh E2E dan CI terasa berat, jangan coba membongkar semuanya sekaligus. Pendekatan bertahap biasanya lebih aman.
1. Petakan suite yang ada
Klasifikasikan semua test ke empat kategori: unit, integrasi, contract, E2E. Banyak tim baru sadar bahwa sebagian besar “integration test” mereka sebenarnya E2E terselubung karena lewat HTTP, database, queue, dan worker sekaligus.
2. Ukur waktu dan flaky rate per kelompok
Identifikasi file atau folder paling lambat dan paling sering gagal. Fokus perbaikan awal pada 10-20% test yang menghabiskan porsi waktu terbesar atau paling sering menghasilkan false alarm.
3. Kurangi E2E yang duplikatif
Jika ada banyak E2E mengulang logika validasi atau variasi kecil dari skenario yang sama, pindahkan variasinya ke unit/integration test. Sisakan E2E untuk alur paling kritikal dan representatif.
4. Tarik logika bisnis keluar dari controller atau handler
Logika yang terkubur di endpoint membuat semua verifikasi harus lewat HTTP. Ekstrak ke service atau domain layer agar bisa diuji cepat lewat unit test.
5. Tambahkan contract test di titik antarsistem
Ini sering mengurangi kebutuhan E2E lintas service yang mahal. Anda tidak perlu selalu menyalakan seluruh stack hanya untuk memastikan schema payload tidak berubah.
6. Bagi jalur CI menjadi cepat dan lengkap
Buat jalur PR yang hemat waktu, lalu jalur pre-release atau nightly yang lebih lengkap. Tim akan lebih mau memelihara test jika feedback harian tetap cepat.
7. Karantina flaky test
Jangan biarkan flaky test tinggal lama di suite utama. Perbaiki segera atau pisahkan sementara dengan label yang jelas. Test yang sering gagal tanpa alasan jelas akan menurunkan kepercayaan seluruh tim pada CI.
8. Ubah incident menjadi test regresi
Setiap bug produksi penting sebaiknya menghasilkan test baru di level yang paling tepat. Ini cara terbaik membuat suite makin relevan terhadap risiko nyata, bukan sekadar bertambah besar.
Kesalahan umum saat menerapkan test pyramid backend
- Mengejar coverage, bukan sinyal.
- Menganggap mock selalu buruk atau selalu baik. Mock berguna pada unit test, tetapi berbahaya jika dipakai untuk menyamarkan integrasi penting.
- Memasukkan semua test ke jalur PR. Akibatnya feedback lambat dan produktivitas turun.
- Tidak membedakan contract test dan E2E. Keduanya menyelesaikan masalah yang berbeda.
- Menguji framework, bukan kode sendiri. Misalnya menulis banyak test hanya untuk memastikan validasi bawaan framework berjalan sesuai dokumentasi.
- Terlalu banyak setup magic. Helper yang terlalu canggih membuat test sulit dipahami dan sulit didiagnosis saat gagal.
Penutup
Test Pyramid Backend yang sehat bukan berarti sedikit E2E semata, tetapi komposisi test yang sesuai risiko dan biaya. Unit test memberi feedback tercepat untuk aturan bisnis. Integration test memastikan batas dengan dependency riil bekerja. Contract test menjaga kesepakatan antarsistem. E2E memverifikasi alur kritikal dari sudut pandang pengguna sistem.
Jika CI Anda mulai lambat dan mahal, biasanya solusi terbaik bukan menambah mesin atau menaikkan paralelisme lebih dulu, melainkan merapikan bentuk piramida: pindahkan verifikasi yang salah level ke lapisan yang lebih tepat, jalankan test sesuai risiko perubahan, dan bedakan jalur PR dari verifikasi sebelum rilis. Dengan begitu, regresi lebih cepat tertangkap tanpa membuat pipeline menjadi beban permanen.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!