Pada Chapter sebelumnya kita melihat runner pengujian bekerja dari balik layar. Sekarang kita fokus pada lapisan terbawah piramida: unit test, yaitu test yang menguji satuan kode terkecil secara terisolasi dari dependensi eksternal. Unit test yang dirancang dengan baik berjalan dalam hitungan milidetik, memberi umpan balik nyaris seketika setelah setiap perubahan kode, dan menunjuk lokasi kegagalan dengan presisi karena lingkup ujinya sempit. Pada Chapter ini kita akan mendefinisikan arti “unit”, mengenal prinsip F.I.R.S.T. sebagai tolok ukur kualitas, membedah anatomi test yang baik dan yang kurang baik, lalu merakit unit test dari Java murni untuk fungsi konversi suhu.
Apa Itu Unit #
Unit adalah potongan kode terkecil yang perilakunya masih berarti untuk diverifikasi secara mandiri — umumnya satu method atau satu class dengan tanggung jawab tunggal. Ciri unit yang sehat adalah ia dapat diuji tanpa menyentuh jaringan, berkas, database, atau jam sistem; seluruh masukan datang dari argumen dan seluruh keluaran keluar dari nilai balik. Fungsi celsiusKeFahrenheit(double) adalah contoh unit ideal: masukan berupa angka, keluaran berupa angka, tanpa efek samping.
Konsekuensinya, kode yang bergantung pada kondisi luar sulit diuji sebagai unit. Method yang membaca jam sistem atau mengirim HTTP membuat hasil uji berubah-ubah antar eksekusi. Solusinya adalah memisahkan logika murni dari efek samping: logika perhitungan ditaruh di method terisolasi yang mudah diuji, sementara akses ke dunia luar dibungkus interface yang bisa diganti test double. Pola pemisahan ini akan kita manfaatkan penuh pada Chapter Mocking Fundamentals.
Prinsip F.I.R.S.T. #
Akronim F.I.R.S.T. merangkum lima sifat unit test yang berkualitas:
| Huruf | Sifat | Arti praktis |
|---|---|---|
| F | Fast | Milidetik per test; ribuan test selesai dalam hitungan detik |
| I | Isolated | Test tidak saling bergantung dan tidak berbagi status |
| R | Repeatable | Hasil sama di komputer mana pun, kapan pun dijalankan |
| S | Self-validating | Test sendiri yang menyatakan lulus atau gagal, tanpa periksa manual |
| T | Timely | Test ditulis bersamaan (atau sebelum) kode produksinya |
Dua sifat yang paling sering terlanggar adalah Isolated dan Repeatable. Test yang meninggalkan sisa data — misalnya baris database dari test sebelumnya — akan membuat test berikutnya lulus atau gagal tergantung urutan jalannya suite. Test yang membaca waktu nyata atau angka acak tanpa seed akan gagal secara acak, membuat tim mulai mengabaikan sinyal kegagalan. Kedua gejala ini lebih berbahaya daripada tidak memiliki test sama sekali karena merusak kepercayaan pada seluruh suite.
Anatomi Test yang Baik #
Bandingkan dua test berikut. Test pertama menguji tiga perilaku sekaligus, memuat logika perulangan, dan memakai angka tanpa penjelasan:
@Test
void testSemua() {
KonversiSuhu k = new KonversiSuhu();
for (double c : new double[] {0, 100, -40}) {
assertTrue(k.celsiusKeFahrenheit(c) > -100);
}
assertEquals(32, k.celsiusKeFahrenheit(0), 0.001);
assertEquals(212, k.celsiusKeFahrenheit(100), 0.001);
assertNotNull(k.kategoriSuhu(50));
}Test kedua memecah perilaku yang sama menjadi test terpisah dengan nama yang menjelaskan maksudnya:
@Test
void titikBekuMenghasilkan32F() {
assertEquals(32.0, KonversiSuhu.celsiusKeFahrenheit(0.0), 0.001);
}
@Test
void suhuNegatifDikategorikanBeku() {
assertEquals("beku", KonversiSuhu.kategoriSuhu(-5.0));
}Ketika test pertama gagal, kita hanya tahu “ada yang salah di antara empat pemeriksaan”. Ketika test kedua gagal, nama test langsung menyebut perilaku yang rusak. Test yang memuat perulangan atau percabangan juga berisiko salah sendiri — test yang butuh di-test bukan test yang baik.
Unit Test dari Java Murni #
Berikut suite unit test utuh untuk dua fungsi konversi suhu, lengkap dengan assertion minimal, pola arrange-act-assert, dan penjalan sederhana — semuanya dalam satu berkas Java murni tanpa dependensi:
import java.util.List;
public class ContohUnitTest {
// ---- unit produksi: fungsi murni tanpa dependensi eksternal ----
static double celsiusKeFahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
static String kategoriSuhu(double celsius) {
if (celsius <= 0) {
return "beku";
}
if (celsius < 100) {
return "cair";
}
return "uap";
}
// ---- assertion minimal ala framework ----
private static void cekSama(Object diharapkan, Object aktual, String namaUji) {
if (!diharapkan.equals(aktual)) {
throw new AssertionError(namaUji + ": diharapkan '"
+ diharapkan + "' tetapi aktual '" + aktual + "'");
}
}
// ---- method uji: satu perilaku per method ----
static void titikBekuMenghasilkan32F() {
double masukan = 0.0; // arrange
double hasil = celsiusKeFahrenheit(masukan); // act
cekSama(32.0, hasil, "titik beku"); // assert
}
static void titikDidihMenghasilkan212F() {
double hasil = celsiusKeFahrenheit(100.0);
cekSama(212.0, hasil, "titik didih");
}
static void minus40SamaDiKeduaSkala() {
double hasil = celsiusKeFahrenheit(-40.0);
cekSama(-40.0, hasil, "minus 40");
}
static void suhuNegatifDikategorikanBeku() {
String kategori = kategoriSuhu(-5.0);
cekSama("beku", kategori, "kategori beku");
}
static void suhuHangatDikategorikanCair() {
String kategori = kategoriSuhu(60.0);
cekSama("cair", kategori, "kategori cair");
}
static void suhuDiAtasDidihDikategorikanUap() {
String kategori = kategoriSuhu(120.0);
cekSama("uap", kategori, "kategori uap");
}
// ---- penjalan sederhana ----
record Uji(String nama, Runnable blok) {}
public static void main(String[] args) {
List<Uji> suite = List.of(
new Uji("titikBekuMenghasilkan32F", ContohUnitTest::titikBekuMenghasilkan32F),
new Uji("titikDidihMenghasilkan212F", ContohUnitTest::titikDidihMenghasilkan212F),
new Uji("minus40SamaDiKeduaSkala", ContohUnitTest::minus40SamaDiKeduaSkala),
new Uji("suhuNegatifDikategorikanBeku", ContohUnitTest::suhuNegatifDikategorikanBeku),
new Uji("suhuHangatDikategorikanCair", ContohUnitTest::suhuHangatDikategorikanCair),
new Uji("suhuDiAtasDidihDikategorikanUap", ContohUnitTest::suhuDiAtasDidihDikategorikanUap)
);
int lulus = 0;
int gagal = 0;
for (Uji uji : suite) {
try {
uji.blok().run();
lulus++;
System.out.println("LULUS " + uji.nama());
} catch (AssertionError e) {
gagal++;
System.out.println("GAGAL " + uji.nama() + " -> " + e.getMessage());
}
}
System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
}
}Output yang Dihasilkan:
LULUS titikBekuMenghasilkan32F
LULUS titikDidihMenghasilkan212F
LULUS minus40SamaDiKeduaSkala
LULUS suhuNegatifDikategorikanBeku
LULUS suhuHangatDikategorikanCair
LULUS suhuDiAtasDidihDikategorikanUap
ringkasan: 6 lulus, 0 gagalEnam test selesai dalam waktu yang tidak terukur oleh mata dan seluruhnya deterministik. Mari kita bedah bagian-bagian pentingnya:
- Baris
static double celsiusKeFahrenheit(...): unit produksi berupa fungsi murni — keluaran hanya bergantung pada masukan, tanpa status bersama, sehingga aman diuji berulang. - Baris
private static void cekSama(...): assertion lokal yang melemparAssertionErrorbila nilai aktual tidak sama dengan harapan; pesannya menyertakan nama uji supaya kegagalan mudah dilacak. - Baris
double masukan = 0.0;sampaicekSama(32.0, ...): ketiga baris membentuk pola arrange-act-assert secara eksplisit dalam test pertama; test lain memakai pola ringkas karena hanya satu baris act. - Baris
record Uji(String nama, Runnable blok) {}: record kecil sebagai pasangan nama-dan-blok memungkinkan suite disusun sebagai daftar terstruktur, bukan serangkaian pemanggilan manual. - Baris
List.of(new Uji(...), ...): seluruh test didaftarkan satu kali; urutan elemenList.ofselalu tetap sehingga laporan konsisten antar eksekusi. - Baris
catch (AssertionError e): penjalan memisahkan kegagalan assertion dari error lain, mencetak pesan penyebab, dan tetap melanjutkan test berikutnya alih-alih menghentikan seluruh suite.
Menjaga Determinisme #
Suite di atas deterministik karena memenuhi tiga syarat: tidak ada pembacaan waktu nyata, tidak ada angka acak tanpa seed, dan tidak ada status yang bocor antar test. Setiap method uji membuat datanya sendiri dan tidak menyimpan apa pun ke field bersama. Bila suatu saat kita perlu menguji kode yang memang bergantung pada keacakan, gunakan new Random(42) dengan seed tetap agar urutan angkanya dapat diprediksi. Prinsip sederhana ini adalah pembeda antara suite yang dipercaya tim dan suite yang diabaikan.
Dengan unit test yang kokoh di dasar piramida, kita siap memakai framework standar industri agar penulisan test menjadi lebih ringkas. Chapter berikutnya memperkenalkan JUnit Jupiter beserta anotasi-anotasinya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohUnitTest.java
Unduh berkasimport java.util.List;
public class ContohUnitTest {
// ---- unit produksi: fungsi murni tanpa dependensi eksternal ----
static double celsiusKeFahrenheit(double celsius) {
return celsius * 9.0 / 5.0 + 32.0;
}
static String kategoriSuhu(double celsius) {
if (celsius <= 0) {
return "beku";
}
if (celsius < 100) {
return "cair";
}
return "uap";
}
// ---- assertion minimal ala framework ----
private static void cekSama(Object diharapkan, Object aktual, String namaUji) {
if (!diharapkan.equals(aktual)) {
throw new AssertionError(namaUji + ": diharapkan '"
+ diharapkan + "' tetapi aktual '" + aktual + "'");
}
}
// ---- method uji: satu perilaku per method ----
static void titikBekuMenghasilkan32F() {
double masukan = 0.0; // arrange
double hasil = celsiusKeFahrenheit(masukan); // act
cekSama(32.0, hasil, "titik beku"); // assert
}
static void titikDidihMenghasilkan212F() {
double hasil = celsiusKeFahrenheit(100.0);
cekSama(212.0, hasil, "titik didih");
}
static void minus40SamaDiKeduaSkala() {
double hasil = celsiusKeFahrenheit(-40.0);
cekSama(-40.0, hasil, "minus 40");
}
static void suhuNegatifDikategorikanBeku() {
String kategori = kategoriSuhu(-5.0);
cekSama("beku", kategori, "kategori beku");
}
static void suhuHangatDikategorikanCair() {
String kategori = kategoriSuhu(60.0);
cekSama("cair", kategori, "kategori cair");
}
static void suhuDiAtasDidihDikategorikanUap() {
String kategori = kategoriSuhu(120.0);
cekSama("uap", kategori, "kategori uap");
}
// ---- penjalan sederhana ----
record Uji(String nama, Runnable blok) {}
public static void main(String[] args) {
List<Uji> suite = List.of(
new Uji("titikBekuMenghasilkan32F", ContohUnitTest::titikBekuMenghasilkan32F),
new Uji("titikDidihMenghasilkan212F", ContohUnitTest::titikDidihMenghasilkan212F),
new Uji("minus40SamaDiKeduaSkala", ContohUnitTest::minus40SamaDiKeduaSkala),
new Uji("suhuNegatifDikategorikanBeku", ContohUnitTest::suhuNegatifDikategorikanBeku),
new Uji("suhuHangatDikategorikanCair", ContohUnitTest::suhuHangatDikategorikanCair),
new Uji("suhuDiAtasDidihDikategorikanUap", ContohUnitTest::suhuDiAtasDidihDikategorikanUap)
);
int lulus = 0;
int gagal = 0;
for (Uji uji : suite) {
try {
uji.blok().run();
lulus++;
System.out.println("LULUS " + uji.nama());
} catch (AssertionError e) {
gagal++;
System.out.println("GAGAL " + uji.nama() + " -> " + e.getMessage());
}
}
System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
}
}