Pengujian Unit

Menguji kode dengan JUnit 5, mulai assertion, pengujian terparameterisasi, alur TDD, dan pengantar Mockito.

Daftar isi

Pengujian unit adalah kode yang memeriksa kode lain secara otomatis, unit demi unit, dan melaporkan hasilnya tanpa perlu dicoba manual satu per satu. Topik ini membahas cara menguji kode Java dengan JUnit 5, mulai dari assertion, pengujian dengan banyak data masukan, alur kerja TDD, sampai pengantar Mockito untuk menggantikan bagian program yang belum mau disentuh. Sebagai pijakan, mekanisme assertion diperlihatkan lebih dahulu dengan pemeriksa sederhana yang bisa dijalankan langsung di terminal, sehingga cara kerja di balik kerangka ujinya terlihat jelas.

Mengapa kode perlu diuji otomatis#

Menguji dengan tangan bekerja untuk program kecil, tetapi pengerjaannya berulang setiap kali kode berubah. Sekali sebuah fitur dipakai ulang di lima tempat, satu perubahan kecil menuntut lima pemeriksaan manual yang mungkin terlewat. Pengujian otomatis memindahkan pemeriksaan itu ke kode, sehingga seluruh rangkaian bisa dijalankan ulang dalam hitungan detik kapan pun diperlukan.

Satu unit test menguji satu unit terkecil yang bisa dipanggil, biasanya satu metode, dengan masukan tertentu dan hasil yang sudah diketahui. Kumpulan pengujian semacam ini memberi dua manfaat yang sulit didapat dari cara lain. Perilaku yang sudah benar tidak rusak diam-diam saat refactoring, dan penulisan uji memaksa kode dirancang agar mudah dipanggil, yang berarti metode kecil dengan tanggung jawab jelas.

Kerangka uji (test framework) adalah library yang mengatur penemuan pengujian, eksekusinya, dan laporan hasilnya. Di ekosistem Java, JUnit 5 adalah kerangka yang paling banyak dipakai, dan Maven menjalankannya pada fase test di lifecycle build.

Assertion: jantung setiap pengujian#

Assertion adalah pernyataan bahwa suatu nilai harus sama dengan nilai yang diharapkan, dan assertion inilah yang menentukan lulus atau gagalnya sebuah pengujian. Mekanismenya selalu sama, yaitu membandingkan nilai ekspektasi dengan nilai aktual lalu mencatat hasilnya. Program berikut meniru mekanisme itu dengan metode pemeriksa sederhana bernama bandingkan, sehingga polanya terlihat tanpa kerangka apa pun.

java UjiKonversiSuhu.java
public class UjiKonversiSuhu {

    static int lulus = 0;
    static int gagal = 0;

    // pemeriksa sederhana, menggantikan peran assertEquals dari JUnit
    static void bandingkan(String namaUji, double diharapkan, double aktual) {
        if (Math.abs(diharapkan - aktual) < 0.0001) {
            System.out.println("LULUS  : " + namaUji);
            lulus++;
        } else {
            System.out.println("GAGAL  : " + namaUji
                    + " (diharapkan " + diharapkan + ", aktual " + aktual + ")");
            gagal++;
        }
    }

    static double celsiusKeFahrenheit(double celsius) {
        return celsius * 9 / 5 + 32;
    }

    public static void main(String[] args) {
        bandingkan("titik beku air", 32.0, celsiusKeFahrenheit(0));
        bandingkan("titik didih air", 212.0, celsiusKeFahrenheit(100));
        bandingkan("nilai negatif", -4.0, celsiusKeFahrenheit(-20));
        System.out.println("total: " + lulus + " lulus, " + gagal + " gagal");
    }
}
text Keluaran
LULUS  : titik beku air
LULUS  : titik didih air
LULUS  : nilai negatif
total: 3 lulus, 0 gagal

Tiga hal pada pemeriksa di atas berlaku sama di JUnit. Nilai ekspektasi ditulis lebih dahulu, perbandingan double memakai batas toleransi alih-alih == karena bilangan pecahan berbasis biner jarang persis sama, dan kegagalan selalu menyertakan kedua nilai agar penyebabnya terbaca. JUnit 5 menyediakan metode-metode assertion serupa di kelas Assertions, antara lain assertEquals, assertTrue, assertThrows, dan assertNull.

Bentuk uji JUnit 5#

Sebuah pengujian JUnit 5 berupa metode biasa yang diberi anotasi @Test di dalam kelas khusus pengujian. Kerangka JUnit menemukan semua metode beranotasi itu, menjalankannya satu per satu, dan mencatat kegagalan assertion sebagai hasil uji. Kelas pengujian biasanya dinamai dengan akhiran Test dan ditempatkan di src/test/java, terpisah dari kode produksi.

java Contoh: kelas uji JUnit 5 untuk konversi suhu
// tidak-dijalankan
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

class KonversiSuhuTest {

    @Test
    void titikBekuAir() {
        assertEquals(32.0, KonversiSuhu.celsiusKeFahrenheit(0));
    }

    @Test
    void nilaiNegatif() {
        assertEquals(-4.0, KonversiSuhu.celsiusKeFahrenheit(-20));
    }
}

Bandingkan isi kedua metode dengan pemeriksa bandingkan pada bagian sebelumnya, dan polanya identik. Perbedaannya terletak pada siapa yang menjalankannya, karena versi JUnit dikeluarkan dari main dan diserahkan ke kerangka uji. Maven menjalankan seluruh kelas Test pada fase test melalui plugin surefire, sehingga pengujian ikut hidup di dalam mvn test maupun mvn package.

Pengujian terparameterisasi#

Pengujian terparameterisasi menjalankan satu logika uji yang sama dengan banyak pasangan data masukan dan hasil, sehingga penyalinan metode uji tidak diperlukan. Pada JUnit 5, bentuknya berupa anotasi @ParameterizedTest yang dipasangkan dengan sumber datanya, dan salah satu sumber yang paling praktis adalah @CsvSource.

java Contoh: parameterized test dengan CsvSource
// tidak-dijalankan
@ParameterizedTest
@CsvSource({
    "0, 32",
    "100, 212",
    "-20, -4"
})
void konversiTunggal(double celsius, double fahrenheit) {
    assertEquals(fahrenheit, KonversiSuhu.celsiusKeFahrenheit(celsius));
}

Anotasi @CsvSource membagi setiap baris menjadi argumen berurutan, sehingga tiga baris data menghasilkan tiga kali eksekusi metode konversiTunggal. Menambah kasus baru kini hanya menambah satu baris teks. Bentuk ini sangat berguna untuk aturan yang punya banyak kombinasi masukan, misalnya batas minimal, pembulatan, atau klasifikasi nilai.

Alur TDD: merah, hijau, dan pemfaktoran ulang#

TDD (test-driven development) adalah alur kerja yang menuliskan pengujian sebelum kode produksinya, dan siklusnya terdiri atas tiga langkah. Langkah merah menulis uji untuk perilaku yang belum ada sehingga ujinya gagal, langkah hijau menulis kode paling sederhana yang membuat uji lulus, dan langkah pemfaktoran ulang merapikan kode tanpa mengubah perilakunya. Siklus ini berputar cepat, biasanya dalam hitungan menit per putaran.

1. Merahtulis uji yang belum lulus2. Hijautulis kode seminimal mungkin3. Pemfaktoran ulangrapikan, perilaku tetap
Siklus TDD dari merah, hijau, sampai pemfaktoran ulang

Tahap merah untuk metode rataRata dapat diperagakan dengan berkas berikut, yang masih memuat implementasi sementara berupa return 0.

java UjiRataRataMerah.java
import java.util.List;

public class UjiRataRataMerah {

    static int lulus = 0;
    static int gagal = 0;

    static void bandingkan(String namaUji, double diharapkan, double aktual) {
        if (Math.abs(diharapkan - aktual) < 0.0001) {
            System.out.println("LULUS  : " + namaUji);
            lulus++;
        } else {
            System.out.println("GAGAL  : " + namaUji
                    + " (diharapkan " + diharapkan + ", aktual " + aktual + ")");
            gagal++;
        }
    }

    // tahap merah: metode belum diimplementasikan dan sementara mengembalikan 0
    static double rataRata(List<Integer> nilai) {
        return 0;
    }

    public static void main(String[] args) {
        bandingkan("satu nilai 80", 80.0, rataRata(List.of(80)));
        bandingkan("dua nilai 70 dan 80", 75.0, rataRata(List.of(70, 80)));
        System.out.println("total: " + lulus + " lulus, " + gagal + " gagal");
    }
}
text Keluaran
GAGAL  : satu nilai 80 (diharapkan 80.0, aktual 0.0)
GAGAL  : dua nilai 70 dan 80 (diharapkan 75.0, aktual 0.0)
total: 0 lulus, 2 gagal

Kegagalan pada tahap merah bukan kegagalan pekerjaan, melainkan tanda bahwa ujinya sungguh menguji sesuatu. Implementasi berikutnya menggantikan isi metode rataRata dengan total dibagi banyaknya elemen, ditambah penjagaan terhadap daftar kosong, sehingga uji yang sama kini lulus penuh.

java UjiRataRataHijau.java
import java.util.List;

public class UjiRataRataHijau {

    static int lulus = 0;
    static int gagal = 0;

    static void bandingkan(String namaUji, double diharapkan, double aktual) {
        if (Math.abs(diharapkan - aktual) < 0.0001) {
            System.out.println("LULUS  : " + namaUji);
            lulus++;
        } else {
            System.out.println("GAGAL  : " + namaUji
                    + " (diharapkan " + diharapkan + ", aktual " + aktual + ")");
            gagal++;
        }
    }

    // tahap hijau: implementasi paling sederhana yang membuat uji lulus
    static double rataRata(List<Integer> nilai) {
        if (nilai.isEmpty()) {
            throw new IllegalArgumentException("daftar nilai kosong");
        }
        int total = 0;
        for (int n : nilai) {
            total += n;
        }
        return (double) total / nilai.size();
    }

    public static void main(String[] args) {
        bandingkan("satu nilai 80", 80.0, rataRata(List.of(80)));
        bandingkan("dua nilai 70 dan 80", 75.0, rataRata(List.of(70, 80)));
        System.out.println("total: " + lulus + " lulus, " + gagal + " gagal");
    }
}
text Keluaran
LULUS  : satu nilai 80
LULUS  : dua nilai 70 dan 80
total: 2 lulus, 0 gagal

Langkah ketiga tidak mengubah hasil uji sama sekali. Nama variabel dirapikan, penjagaan dipindahkan bila perlu, dan pengujian dijalankan ulang untuk memastikan perilakunya tetap. Keamanan itulah hadiah utama TDD, karena setiap penataan ulang selalu berjalan di bawah pengaman berupa uji yang siap berteriak saat perilaku bergeser.

Pengantar Mockito#

Sering kali unit yang diuji bergantung pada bagian lain yang lambat, berubah-ubah, atau belum selesai, misalnya akses database atau layanan jaringan. Mock adalah objek palsu yang menggantikan dependensi itu, dan Mockito adalah library Java yang paling umum untuk membuatnya. Objek mock diperintahkan apa yang harus dikembalikan pada panggilan tertentu, sehingga unit yang diuji benar-benar terisolasi.

java Contoh: menggantikan dependensi dengan mock Mockito
// tidak-dijalankan
// Mockito tersedia sebagai dependensi Maven, bukan bagian dari JDK
@Test
void totalBelanjaMenghitungHargaDariGudang() {
    Gudang gudang = mock(Gudang.class);
    when(gudang.hargaSatuan("Buku tulis")).thenReturn(5000);

    Kasir kasir = new Kasir(gudang);
    assertEquals(10000, kasir.totalBelanja(List.of("Buku tulis", "Buku tulis")));

    verify(gudang).hargaSatuan("Buku tulis");
}

Empat baris di atas menunjukkan pola lengkapnya. mock membuat objek palsu bertipe Gudang, when(...).thenReturn(...) menetapkan jawabannya, Kasir diuji memakai objek palsu itu tanpa menyentuh data sungguhan, dan verify memastikan metode yang diharapkan memang dipanggil. Pengujian seperti ini berjalan seketika dan tidak mungkin gagal karena jaringan atau isi database, sehingga kegagalan selalu menunjuk pada logika unit yang sedang diuji.

Kesalahan umum#

  • Menulis uji yang bergantung urutan eksekusi. Uji kedua yang hanya lulus bila uji pertama berjalan dulu akan kacau saat dijalankan terpisah. Buat setiap uji menyiapkan keadaannya sendiri.
  • Menganggap uji GAGAL adalah tanda bekerja tanpa benar. Pada alur TDD, gagal dulu justru bagian dari proses, sedangkan uji yang langsung lulus tanpa pernah gagal patut dicurigai tidak menguji apa pun.
  • Menyalin-paste metode uji untuk setiap kasus data. Perulangan salinan membuat penambahan kasus terasa berat, padahal @ParameterizedTest melayaninya dengan satu baris data.
  • Mengejar angka cakupan kode sebagai tujuan. Cakupan tinggi dengan uji yang tidak menegaskan apa pun hanya menambah waktu build. Uji yang baik memeriksa perilaku, bukan sekadar melewati baris.
  • Menguji detail implementasi alih-alih hasil kerjanya. Uji yang menempel pada cara kerja internal patah setiap refactoring, meskipun perilakunya tidak berubah.

Ringkasan#

  • Pengujian unit adalah kode yang memeriksa perilaku unit lain secara otomatis, dan assertion adalah inti yang menentukan lulus atau gagalnya.
  • JUnit 5 menjalankan metode beranotasi @Test, sedangkan @ParameterizedTest dengan @CsvSource melayani satu logika uji untuk banyak data.
  • Alur TDD berputar pada merah, hijau, dan pemfaktoran ulang, sehingga refactoring selalu berjalan di bawah pengaman uji.
  • Mockito menggantikan dependensi dengan mock yang jawabannya ditetapkan di dalam uji, sehingga unit benar-benar terisolasi.

Referensi#