Assertion adalah pernyataan bahwa kondisi tertentu harus benar pada titik tertentu dalam test; begitu kondisinya salah, test gagal dengan pesan yang menjelaskan ketidaksesuaiannya. Kualitas suite uji sangat ditentukan oleh kualitas assertion-nya: assertion yang tepat menunjuk kegagalan dalam satu baris laporan, sedangkan assertion yang kabur hanya memberi sinyal “ada yang salah entah di mana”. Pada Chapter ini kita akan membedah assertion bawaan JUnit, belajar menulis pesan kegagalan yang membantu, mengenal gaya penulisan fluent dari AssertJ, lalu memahami pemeriksaan berkelompok dengan assertAll — dan membuktikan mekanismenya dengan framework assertion mini dari Java murni.

Assertion Dasar JUnit #

Tiga assertion JUnit yang paling sering dipakai adalah assertEquals, assertTrue, dan assertThrows. Seluruhnya berupa static method di class org.junit.jupiter.api.Assertions:

java
import static org.junit.jupiter.api.Assertions.*;

class PelangganTest {

    @Test
    void namaDanKotaSesuaiData() {
        Pelanggan budi = cariPelanggan("PL-001");

        assertEquals("Budi", budi.nama());
        assertEquals("Bandung", budi.kota(), "kota pada data PL-001");
        assertTrue(budi.email().endsWith("@example.com"));
    }

    @Test
    void pelangganTidakDikenalDitolak() {
        IllegalArgumentException e = assertThrows(
            IllegalArgumentException.class,
            () -> cariPelanggan("PL-999"));
        assertEquals("pelanggan tidak ditemukan: PL-999", e.getMessage());
    }
}

Method assertEquals menerima dua argumen: nilai yang diharapkan di posisi pertama, nilai aktual di posisi kedua. Method assertThrows menerima class exception dan sebuah lambda; test lulus bila lambda melempar exception bertipe tersebut, dan instance exception dikembalikan sehingga pesannya bisa diperiksa lanjut. Urutan argumen yang terbalik adalah kesalahan paling umum pemula — pesan kegagalannya menjadi “diharapkan Bandung tetapi aktual Budi” yang menyesatkan saat dibaca cepat.

Pesan Kegagalan yang Membantu #

Pesan kegagalan default berisi nilai yang dibandingkan, tetapi pesan buatan kita sendiri dapat menyertakan konteks: id data, prasyarat, atau nomor tiket bug. JUnit menempatkan pesan sebagai argumen terakhir sehingga tetap terbaca sebagai penjelasan, bukan pengganggu:

java
assertEquals("Bandung", budi.kota(),
    "kota pelanggan PL-001 berubah sejak migrasi data");

Pesan yang baik menjawab pertanyaan “apa yang harusnya terjadi dan mengapa penting”. Hindari pesan yang hanya mengulang nama method — laporan sudah mencantumkan nama testnya. Manfaat pesan kustom paling terasa saat suite dijalankan otomatis di CI dan satu-satunya informasi yang dibaca engineer adalah baris kegagalan tersebut.

AssertJ dan Gaya Fluent #

AssertJ adalah pustaka assertion dengan API fluent yang membaca seperti kalimat. Rantai method-nya menawarkan pelengkapan kode yang kaya di IDE dan pesan kegagalan yang mendeskripsikan diri sendiri:

java
import static org.assertj.core.api.Assertions.assertThat;

class PelangganTestAssertJ {

    @Test
    void dataPelangganLengkap() {
        Pelanggan budi = cariPelanggan("PL-001");

        assertThat(budi.nama()).isEqualTo("Budi");
        assertThat(budi.email())
            .startsWith("budi")
            .endsWith("@example.com");
        assertThat(budi.kota()).isIn("Bandung", "Jakarta");
    }
}

Perhatikan bahwa satu baris assertThat(...).startsWith(...).endsWith(...) memeriksa dua kondisi berurutan pada objek yang sama. Saat gagal, AssertJ mencetak nilai aktual lengkap beserta bagian yang dibandingkan — informasi yang sering lebih kaya daripada assertion bawaan. AssertJ ditambahkan sebagai dependensi test terpisah dan boleh dipakai berdampingan dengan assertion JUnit dalam satu class.

Pemeriksaan Berkelompok dengan assertAll #

Assertion biasa bersifat fail-fast: eksekusi berhenti di kegagalan pertama, sehingga pemeriksaan setelahnya tidak pernah berjalan. Untuk verifikasi data yang kompleks, JUnit menyediakan assertAll yang menjalankan seluruh blok pemeriksaan dan melaporkan semua kegagalan sekaligus:

java
@Test
void dataPelangganUtuh() {
    Pelanggan budi = cariPelanggan("PL-001");

    assertAll("data pelanggan PL-001",
        () -> assertEquals("Budi", budi.nama()),
        () -> assertEquals("Jakarta", budi.kota()),
        () -> assertEquals("[email protected]", budi.email())
    );
}

Mekanisme di balik layarnya sederhana: setiap lambda dijalankan satu per satu dalam blok try-catch yang menampung AssertionError, lalu seluruh kegagalan dikumpulkan dan dilaporkan bersama. Kita dapat membuktikannya dengan versi Java murni berikut:

import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ContohAssertLengkap {

    record Pelanggan(String id, String nama, String email, String kota) {}

    private static final Map<String, Pelanggan> BASIS_DATA = Map.of(
        "PL-001", new Pelanggan("PL-001", "Budi", "[email protected]", "Bandung"),
        "PL-002", new Pelanggan("PL-002", "Siti", "[email protected]", "Surabaya")
    );

    static Pelanggan cariPelanggan(String id) {
        Pelanggan p = BASIS_DATA.get(id);
        if (p == null) {
            throw new IllegalArgumentException("pelanggan tidak ditemukan: " + id);
        }
        return p;
    }

    // ---- assertion mini ala framework ----

    private static void sama(Object diharapkan, Object aktual) {
        if (!diharapkan.equals(aktual)) {
            throw new AssertionError("diharapkan '" + diharapkan
                + "' tetapi aktual '" + aktual + "'");
        }
    }

    private static void benar(boolean kondisi, String pesan) {
        if (!kondisi) {
            throw new AssertionError(pesan);
        }
    }

    // ---- assertAll mini: jalankan semua, kumpulkan kegagalan ----

    private static void grup(String nama, Runnable... pemeriksaan) {
        List<String> kegagalan = new ArrayList<>();
        for (Runnable p : pemeriksaan) {
            try {
                p.run();
            } catch (AssertionError e) {
                kegagalan.add(e.getMessage());
            }
        }
        if (kegagalan.isEmpty()) {
            System.out.println(nama + ": semua pemeriksaan lulus");
        } else {
            System.out.println(nama + ": " + kegagalan.size() + " pemeriksaan gagal");
            for (String k : kegagalan) {
                System.out.println("  - " + k);
            }
        }
    }

    public static void main(String[] args) {
        Pelanggan budi = cariPelanggan("PL-001");

        // pemeriksaan tunggal: eksekusi berhenti di kegagalan pertama
        System.out.println("-- pemeriksaan tunggal --");
        try {
            sama("Jakarta", budi.kota());
            System.out.println("kota sesuai harapan");
        } catch (AssertionError e) {
            System.out.println("terhenti di: " + e.getMessage());
        }

        // pemeriksaan berkelompok: seluruh pemeriksaan tetap dijalankan
        System.out.println("-- pemeriksaan berkelompok --");
        grup("data pelanggan PL-001",
            () -> sama("Budi", budi.nama()),
            () -> sama("Jakarta", budi.kota()),
            () -> benar(budi.email().endsWith("@example.com"),
                "email harus diakhiri @example.com"),
            () -> sama("[email protected]", budi.email())
        );
    }
}

Output yang Dihasilkan:

text
-- pemeriksaan tunggal --
terhenti di: diharapkan 'Jakarta' tetapi aktual 'Bandung'
-- pemeriksaan berkelompok --
data pelanggan PL-001: 2 pemeriksaan gagal
  - diharapkan 'Jakarta' tetapi aktual 'Bandung'
  - diharapkan '[email protected]' tetapi aktual '[email protected]'

Bagian pemeriksaan tunggal berhenti di kegagalan pertama, sedangkan pemeriksaan berkelompok tetap mengeksekusi empat lambda dan melaporkan dua kegagalan sekaligus. Mari kita bedah baris-per-baris bagian kuncinya:

  1. Baris private static void sama(Object diharapkan, Object aktual): padanan assertEquals — membandingkan dengan equals dan melempar AssertionError berisi kedua nilai saat tidak sama.
  2. Baris private static void grup(String nama, Runnable... pemeriksaan): padanan assertAll — menerima judul dan sejumlah lambda pemeriksaan melalui varargs.
  3. Baris for (Runnable p : pemeriksaan): setiap lambda dijalankan; AssertionError yang tertangkap tidak menghentikan perulangan melainkan ditampung ke List<String> kegagalan.
  4. Baris if (kegagalan.isEmpty()): cabang lulus dan gagal dipisah; jumlah kegagalan dicetak sebelum daftarnya, meniru format laporan assertAll yang merangkum dulu lalu merinci.
  5. Baris grup("data pelanggan PL-001", () -> ..., () -> ...): empat lambda mengeksekusi pemeriksaan independen; lambda kedua dan keempat gagal secara sengaja untuk memperlihatkan bahwa kegagalan pertama tidak menutup pemeriksaan berikutnya.

Konsep serapan dari AssertJ bernama soft assertions memakai ide yang sama dengan bentuk berbeda: objek SoftAssertions mengumpulkan kegagalan dari rantai pemeriksaan, lalu satu panggilan assertAll() di akhir test memverifikasi seluruh tumpukan. Soft assertions cocok untuk verifikasi bertahap satu objek, sedangkan assertAll JUnit cocok untuk memeriksa banyak aspek independen sekaligus.

Dengan assertion yang tajam, test berikutnya yang akan kita pelajari menghemat penulisan: satu method uji yang menerima banyak kombinasi data lewat parameterized test.

Referensi #

Berkas Sumber

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

ContohAssertLengkap.java

Unduh berkas
java
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

public class ContohAssertLengkap {

    record Pelanggan(String id, String nama, String email, String kota) {}

    private static final Map<String, Pelanggan> BASIS_DATA = Map.of(
        "PL-001", new Pelanggan("PL-001", "Budi", "[email protected]", "Bandung"),
        "PL-002", new Pelanggan("PL-002", "Siti", "[email protected]", "Surabaya")
    );

    static Pelanggan cariPelanggan(String id) {
        Pelanggan p = BASIS_DATA.get(id);
        if (p == null) {
            throw new IllegalArgumentException("pelanggan tidak ditemukan: " + id);
        }
        return p;
    }

    // ---- assertion mini ala framework ----

    private static void sama(Object diharapkan, Object aktual) {
        if (!diharapkan.equals(aktual)) {
            throw new AssertionError("diharapkan '" + diharapkan
                + "' tetapi aktual '" + aktual + "'");
        }
    }

    private static void benar(boolean kondisi, String pesan) {
        if (!kondisi) {
            throw new AssertionError(pesan);
        }
    }

    // ---- assertAll mini: jalankan semua, kumpulkan kegagalan ----

    private static void grup(String nama, Runnable... pemeriksaan) {
        List<String> kegagalan = new ArrayList<>();
        for (Runnable p : pemeriksaan) {
            try {
                p.run();
            } catch (AssertionError e) {
                kegagalan.add(e.getMessage());
            }
        }
        if (kegagalan.isEmpty()) {
            System.out.println(nama + ": semua pemeriksaan lulus");
        } else {
            System.out.println(nama + ": " + kegagalan.size() + " pemeriksaan gagal");
            for (String k : kegagalan) {
                System.out.println("  - " + k);
            }
        }
    }

    public static void main(String[] args) {
        Pelanggan budi = cariPelanggan("PL-001");

        // pemeriksaan tunggal: eksekusi berhenti di kegagalan pertama
        System.out.println("-- pemeriksaan tunggal --");
        try {
            sama("Jakarta", budi.kota());
            System.out.println("kota sesuai harapan");
        } catch (AssertionError e) {
            System.out.println("terhenti di: " + e.getMessage());
        }

        // pemeriksaan berkelompok: seluruh pemeriksaan tetap dijalankan
        System.out.println("-- pemeriksaan berkelompok --");
        grup("data pelanggan PL-001",
            () -> sama("Budi", budi.nama()),
            () -> sama("Jakarta", budi.kota()),
            () -> benar(budi.email().endsWith("@example.com"),
                "email harus diakhiri @example.com"),
            () -> sama("[email protected]", budi.email())
        );
    }
}