Test yang baik berangkat dari kondisi awal yang bersih dan tidak meninggalkan sisa setelah selesai. Sayangnya, kondisi bersih jarang datang gratis: keranjang uji perlu direset, sambungan basis data perlu dibuka lalu ditutup, berkas sementara perlu dihapus. JUnit Jupiter menyediakan empat anotasi siklus hidup — @BeforeAll, @BeforeEach, @AfterEach, dan @AfterAll — sebagai tempat terpusat untuk pekerjaan penyiapan dan pembersihan tersebut. Pada Chapter ini kita akan memetakan keempat anotasi itu, membuktikan urutan eksekusinya dengan demo Java murni yang mencetak setiap tahap, lalu belajar memilih hook yang tepat agar test tetap terisolasi.

Empat Anotasi Siklus Hidup #

Keempat hook dikelompokkan menjadi dua pasang. Pasangan level class berjalan sekali untuk seluruh class uji; pasangan level test berjalan mengelilingi setiap method uji:

AnotasiBerjalanKegunaan umumSyarat default
@BeforeAllSekali sebelum seluruh testMembuka sambungan, memuat berkas besarHarus static
@BeforeEachSebelum tiap testMereset state, membuat objek uji segarBoleh non-static
@AfterEachSetelah tiap testMembersihkan sisa per testBoleh non-static
@AfterAllSekali setelah seluruh testMenutup sambungan, mematikan sumber dayaHarus static

Satu detail yang sering mengejutkan pemula: Jupiter membuat instance baru dari class uji untuk setiap method test. Field instance kembali ke nilai awal untuk tiap test tanpa perlu usaha apa pun dari kita — isolasi gratis. State yang benar-benar bersama antar test hanya ada pada field static, dan di situlah hook level class berperan.

Demo Urutan dari Java Murni #

Daripada menghafal diagram, kita buktikan urutannya. Program berikut menyalin perilaku Jupiter: satu penyiapan level class, instance segar per test, dan pembersihan level test di sekitar setiap method uji:

import java.util.List;
import java.util.function.Consumer;

public class ContohSiklusUji {

    private static int jumlahInstance = 0;

    private final int nomorInstance;

    ContohSiklusUji() {
        jumlahInstance++;
        nomorInstance = jumlahInstance;
    }

    // ---- padanan @BeforeAll dan @AfterAll: sekali untuk seluruh class ----

    static void sebelumSemua() {
        System.out.println("@BeforeAll  : membuka sambungan ke basis data uji");
    }

    static void sesudahSemua() {
        System.out.println("@AfterAll   : menutup sambungan ke basis data uji");
    }

    // ---- padanan @BeforeEach dan @AfterEach: sekali untuk tiap test ----

    void sebelumTiap() {
        System.out.println("@BeforeEach : siapkan instance uji ke-" + nomorInstance);
    }

    void sesudahTiap() {
        System.out.println("@AfterEach  : bersihkan instance uji ke-" + nomorInstance);
    }

    // ---- dua method uji ----

    void ujiTambahSatuItem() {
        System.out.println("@Test       : ujiTambahSatuItem");
    }

    void ujiKeranjangKosong() {
        System.out.println("@Test       : ujiKeranjangKosong");
    }

    public static void main(String[] args) {
        sebelumSemua();

        List<Consumer<ContohSiklusUji>> semuaUji = List.of(
            uji -> uji.ujiTambahSatuItem(),
            uji -> uji.ujiKeranjangKosong()
        );

        for (Consumer<ContohSiklusUji> uji : semuaUji) {
            ContohSiklusUji instance = new ContohSiklusUji(); // instance segar per test
            instance.sebelumTiap();
            uji.accept(instance);
            instance.sesudahTiap();
        }

        sesudahSemua();
    }
}

Output yang Dihasilkan:

text
@BeforeAll  : membuka sambungan ke basis data uji
@BeforeEach : siapkan instance uji ke-1
@Test       : ujiTambahSatuItem
@AfterEach  : bersihkan instance uji ke-1
@BeforeEach : siapkan instance uji ke-2
@Test       : ujiKeranjangKosong
@AfterEach  : bersihkan instance uji ke-2
@AfterAll   : menutup sambungan ke basis data uji

Urutan cetakan itu adalah kontrak yang dipegang runner Jupiter. Bedah baris-per-baris untuk bagian paling penting:

  1. Baris private static int jumlahInstance = 0;: penghitung level class — padanan field static yang nilainya bertahan lintas instance, dipakai untuk menomori setiap instance uji.
  2. Baris isi constructor jumlahInstance++; nomorInstance = jumlahInstance;: setiap kali runner membuat instance baru, nomor berjalan maju; dari sini kita bisa memverifikasi bahwa dua test berjalan pada dua instance berbeda.
  3. Baris static void sebelumSemua() dan sesudahSemua(): padanan @BeforeAll/@AfterAll — dibuat static meniru syarat default Jupiter untuk hook level class.
  4. Baris ContohSiklusUji instance = new ContohSiklusUji();: inti siklus Jupiter — satu instance segar per test, diletakkan di dalam perulangan sehingga state instance tidak bocor ke test berikutnya.
  5. Baris instance.sebelumTiap(); ... instance.sesudahTiap();: hook level test mengelilingi tepat satu method uji; jika ada tiga test, pasangan ini berjalan tiga kali.
  6. Baris sesudahSemua(); di akhir main: pembersihan level class tetap berjalan setelah seluruh test selesai — bahkan pada Jupiter, @AfterAll tetap dipanggil walau ada test yang gagal di tengah jalan.

Kapan Memakai Hook yang Mana #

Aturan praktisnya mudah: mulai dari @BeforeEach untuk semua kebutuhan penyiapan, naik ke @BeforeAll hanya jika penyiapan itu mahal dan hasilnya berupa data baca-saja. Membuka koneksi HTTP atau memuat model machine learning sekali untuk dipakai ratusan test adalah kandidat @BeforeAll yang tepat. Sebaliknya, objek yang akan dimodifikasi test — keranjang, rekening, sesi — sebaiknya dibuat di @BeforeEach atau langsung di field instance agar tiap test menerima salinan segar.

Siklus PER_CLASS #

Sejak JUnit 5, syarat static pada hook level class dapat dilonggarkan dengan mengubah mode lifecycle class uji:

java
@TestInstance(TestInstance.Lifecycle.PER_CLASS)
class LaporanUjiTest {

    @BeforeAll
    void sekaliSebelumSemua() {  // kini boleh non-static
        // memuat data yang dibutuhkan seluruh test
    }
}

Dengan PER_CLASS, runner memakai satu instance untuk seluruh test di class tersebut — menguntungkan bila class uji membawa state mahal yang ingin dibangun sekali, tetapi menambah risiko kebocoran state antar test. Mode default PER_METHOD tetap menjadi pilihan teraman untuk mayoritas kasus; perlakukan PER_CLASS sebagai optimasi yang disengaja, bukan kebiasaan.

Sekarang kita memahami kapan penyiapan berjalan. Chapter berikutnya menaikkan kualitas penyiapan itu sendiri: menyusun test fixture yang stabil dan mudah dirawat lewat builder dan object mother.

Referensi #

Berkas Sumber

Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.

ContohSiklusUji.java

Unduh berkas
java
import java.util.List;
import java.util.function.Consumer;

public class ContohSiklusUji {

    private static int jumlahInstance = 0;

    private final int nomorInstance;

    ContohSiklusUji() {
        jumlahInstance++;
        nomorInstance = jumlahInstance;
    }

    // ---- padanan @BeforeAll dan @AfterAll: sekali untuk seluruh class ----

    static void sebelumSemua() {
        System.out.println("@BeforeAll  : membuka sambungan ke basis data uji");
    }

    static void sesudahSemua() {
        System.out.println("@AfterAll   : menutup sambungan ke basis data uji");
    }

    // ---- padanan @BeforeEach dan @AfterEach: sekali untuk tiap test ----

    void sebelumTiap() {
        System.out.println("@BeforeEach : siapkan instance uji ke-" + nomorInstance);
    }

    void sesudahTiap() {
        System.out.println("@AfterEach  : bersihkan instance uji ke-" + nomorInstance);
    }

    // ---- dua method uji ----

    void ujiTambahSatuItem() {
        System.out.println("@Test       : ujiTambahSatuItem");
    }

    void ujiKeranjangKosong() {
        System.out.println("@Test       : ujiKeranjangKosong");
    }

    public static void main(String[] args) {
        sebelumSemua();

        List<Consumer<ContohSiklusUji>> semuaUji = List.of(
            uji -> uji.ujiTambahSatuItem(),
            uji -> uji.ujiKeranjangKosong()
        );

        for (Consumer<ContohSiklusUji> uji : semuaUji) {
            ContohSiklusUji instance = new ContohSiklusUji(); // instance segar per test
            instance.sebelumTiap();
            uji.accept(instance);
            instance.sesudahTiap();
        }

        sesudahSemua();
    }
}