Mengapa Unit Test Harus Berada di Tahap Awal Pipeline Anda
Bayangkan Anda melakukan push kode pada Jumat sore. Build berhasil, deployment berjalan lancar, dan Anda pulang. Sabtu pagi, ponsel Anda berbunyi dengan notifikasi. Sebuah perhitungan diskon menerapkan nilai negatif ke pesanan pelanggan. Logikanya terlihat benar saat review. Tapi tidak ada yang menangkap kasus tepi di mana kode kupon digabungkan dengan harga diskon menghasilkan total negatif.
Inilah jenis masalah yang unit test ada untuk menangkapnya. Bukan karena mereka canggih, tetapi karena mereka berjalan cepat, berjalan lebih awal, dan berjalan secara terisolasi. Mereka adalah garis pertahanan pertama dalam pipeline pengiriman mana pun.
Apa yang Sebenarnya Diperiksa oleh Unit Test
Sebuah unit test memverifikasi satu perilaku bermakna dari titik masuk di mana perilaku tersebut dimulai. Dalam layanan backend, titik masuk itu bisa berupa REST endpoint atau use case. Request diizinkan untuk melewati lapisan internal yang sebenarnya: controller, service, domain logic, dan repository boundary. Test tidak bertujuan membuktikan bahwa satu method memanggil method lain. Test bertujuan membuktikan bahwa sistem merespons dengan benar terhadap input yang bermakna.
Jika Anda memiliki fungsi yang menghitung biaya pengiriman berdasarkan berat dan tujuan, unit test dapat memastikan:
- Berat standar mengembalikan biaya standar
- Berat nol mengembalikan biaya nol
- Berat negatif mengembalikan error atau nol
- Berat maksimum mengembalikan nilai batas
Apa yang tidak boleh dibuktikan oleh unit test adalah apakah database nyata menyimpan biaya tersebut dengan benar, apakah payment gateway nyata menerimanya, atau apakah layanan tetangga benar-benar tersedia. Itu adalah urusan untuk jenis test lainnya.
Nilai unit test sempit tetapi dalam. Mereka memberi Anda keyakinan bahwa perilaku tertentu masih berfungsi ketika sistem di sekitarnya terkendali. Ketika Anda mengubah kode nanti, unit test yang lolos memberi tahu Anda bahwa Anda tidak merusak perilaku yang sudah diverifikasi.
Prinsip Isolasi
Agar unit test cepat dan andal, dunia di sekitarnya harus terkendali. Lapisan aplikasi internal harus berjalan normal. Tetangga eksternal tidak boleh menentukan hasil test. Itu biasanya berarti tidak ada koneksi database produksi nyata, tidak ada panggilan HTTP pihak ketiga langsung, dan tidak ada ketergantungan pada layanan lain yang berjalan. Jika perilaku membutuhkan data, test dapat menggunakan data terkendali, database test in-memory/lokal, atau fake, mock, atau stub di batas sistem.
Inilah mengapa unit test tidak boleh didefinisikan sebagai "satu test per method" atau "satu test per class." Definisi itu terlalu mekanis dan sering mendorong tim untuk menguji detail implementasi. Unit test lebih baik dipahami sebagai test perilaku dari titik masuk yang relevan, dengan dunia eksternal yang cukup terkendali sehingga kegagalan mengarah kembali ke perilaku yang sedang diuji.
Kode mobile adalah contoh yang berguna. Beberapa perilaku hanya masuk akal di dalam runtime mobile. Dalam kasus itu, menggunakan emulator atau simulator tidak secara otomatis membuat test menjadi salah. Pertanyaannya adalah apakah test masih fokus pada satu perilaku dan mengontrol dependensi di sekitarnya. Jika ya, test tersebut masih dapat memenuhi tujuan unit test dalam pipeline.
Isolasi inilah yang membuat unit test cepat. Rangkaian unit test yang ditulis dengan baik untuk layanan backend tipikal selesai dalam hitungan detik, bukan menit. Bandingkan dengan integration test yang memutar container atau terhubung ke database test. Itu membutuhkan waktu menit.
Kecepatan penting karena test cepat lebih sering dijalankan. Developer menjalankannya secara lokal sebelum push kode. Pipeline CI menjalankannya segera setelah build. Jika ada yang rusak, Anda tahu dalam hitungan detik atau menit, bukan setelah menunggu rangkaian test lengkap yang memakan waktu setengah jam.
Di Mana Unit Test Cocok dalam Pipeline
Unit test berada di tahap paling awal pipeline Anda, tepat setelah kode dikompilasi atau di-build. Logikanya sederhana: jika perilaku dasar yang diekspos oleh aplikasi sudah rusak, tidak ada gunanya menjalankan test yang lebih lambat yang bergantung pada sistem tetangga nyata.
Urutan tahap pipeline tipikal terlihat seperti ini:
Berikut adalah contoh praktis bagaimana langkah pertama itu terlihat dalam file konfigurasi CI:
# .gitlab-ci.yml atau konfigurasi CI serupa
stages:
- build
- test
- deploy
build:
stage: build
script:
- npm install
- npm run build
test-unit:
stage: test
script:
- npm test -- --coverage
only:
- merge_requests
- main
- Build atau kompilasi kode
- Jalankan unit test
- Jalankan static analysis atau linting
- Build container images atau artifacts
- Jalankan integration test
- Deploy ke staging
- Jalankan end-to-end atau acceptance test
- Deploy ke production
Jika unit test gagal di langkah 2, pipeline berhenti. Tidak ada container yang dibangun. Tidak ada lingkungan staging yang terpakai. Tidak ada waktu yang terbuang menunggu integration test yang pasti akan gagal karena logika dasarnya salah.
Inilah fast feedback loop. Semakin awal Anda menangkap bug, semakin murah biaya perbaikannya. Bug yang ditemukan selama unit test membutuhkan waktu menit untuk diperbaiki. Bug yang ditemukan di production membutuhkan respons insiden, prosedur rollback, komunikasi pelanggan, dan mungkin perbaikan data.
Kapan Unit Test Tidak Cukup
Unit test memiliki titik buta: mereka tidak dapat memverifikasi bahwa komponen bekerja bersama dalam sistem nyata. Jika perilaku checkout Anda bergantung pada payment API eksternal, unit test dapat memeriksa bagaimana kode Anda berperilaku ketika dependensi pembayaran mengembalikan sukses, gagal, timeout, atau data yang salah format. Unit test tidak dapat memberi tahu Anda apakah payment API nyata menerima format request Anda, menangani autentikasi, atau mengembalikan struktur respons yang diharapkan.
Untuk itu, Anda membutuhkan integration test. Tapi unit test masih memiliki tujuan di sini. Mereka memverifikasi bahwa struktur kode Anda benar, parameter dilewatkan dalam urutan yang tepat, dan penanganan error berfungsi seperti yang diharapkan. Mereka hanya tidak dapat menggantikan pemeriksaan integrasi nyata.
Keterbatasan lain adalah unit test tidak dapat menangkap masalah konfigurasi, perbedaan lingkungan, atau masalah infrastruktur. Fungsi yang bekerja sempurna di unit test mungkin gagal di production karena database production memiliki pengaturan collation yang berbeda, atau karena variabel lingkungan yang diperlukan hilang. Masalah-masalah itu membutuhkan pendekatan testing yang berbeda.
Seberapa Banyak Unit Test yang Cukup
Jawabannya tergantung pada risiko. Jika sebuah fungsi mengimplementasikan logika bisnis inti di mana kesalahan dapat menyebabkan kerugian finansial, korupsi data, atau masalah keamanan, Anda ingin unit test yang menyeluruh mencakup kasus normal, kasus tepi, kasus error, dan kondisi batas.
Jika sebuah fungsi hanya melewatkan data dari satu tempat ke tempat lain tanpa transformasi, satu unit test yang memastikan pass-through bekerja dengan benar mungkin sudah cukup. Menghabiskan waktu berjam-jam menulis test yang ekshaustif untuk kode sepele bukanlah penggunaan waktu yang baik.
Pendekatan praktis adalah risk-based testing. Identifikasi bagian mana dari basis kode Anda yang memiliki risiko tertinggi jika gagal. Fokuskan upaya unit testing Anda di sana. Untuk kode berisiko rendah, tulis test secukupnya untuk menangkap kesalahan yang jelas.
Checklist Praktis untuk Unit Test dalam Pipeline Anda
- Unit test dijalankan sebelum integration test atau end-to-end test
- Unit test selesai dalam beberapa menit untuk seluruh basis kode
- Unit test tidak memerlukan layanan eksternal, database, atau akses jaringan
- Setiap unit test memverifikasi satu perilaku bermakna dari titik masuk yang relevan, bukan satu method atau class
- Test bersifat deterministik: input yang sama selalu menghasilkan output yang sama
- Unit test yang gagal menghentikan pipeline segera
- Developer dapat menjalankan test yang sama secara lokal sebelum push
Intisari Konkret
Unit test bukan tentang mencapai angka coverage yang sempurna. Mereka tentang menangkap kelas bug yang paling umum sedini mungkin, dengan biaya waktu dan infrastruktur yang paling sedikit. Tempatkan mereka pertama dalam pipeline Anda, jaga agar tetap cepat, tetap terisolasi, dan fokuskan pada logika yang paling penting. Semua yang lain dibangun di atas fondasi itu.