Dua Chapter sebelumnya kita menulis runner pengujian sendiri supaya konsepnya transparan. Di dunia profesional, pekerjaan tersebut sudah diselesaikan oleh framework matang: JUnit. Versi JUnit 5 — yang secara resmi bernama JUnit 5 dengan subproyek JUnit Jupiter sebagai model pemrograman dan ekstensinya — menjadi standar de facto pengujian Java dan ditopang seluruh build tool serta IDE utama. Pada Chapter ini kita akan memahami arsitektur JUnit 5, menata direktori test yang paralel dengan direktori kode utama, mengenal anotasi inti seperti @Test, @DisplayName, @Disabled, dan @Nested, lalu membuktikan bahwa anotasi Jupiter hanyalah bentuk lain dari reflection yang sudah kita kuasai.
Arsitektur Tiga Subproyek #
JUnit 5 terdiri dari tiga bagian yang bekerja bersama. JUnit Platform adalah fondasi peluncur test yang dikenali Maven, Gradle, dan IDE. JUnit Jupiter menyediakan model pemrograman baru — anotasi @Test, assertion, dan extension. JUnit Vintage menjalankan test lawas berbasis JUnit 3 dan 4 sehingga proyek lama bisa bermigrasi bertahap. Pemisahan ini berarti platform tidak terikat satu API test; framework lain seperti jqwik (Chapter Property-Based Testing) ikut berjalan di atas platform yang sama.
Untuk memakai Jupiter dalam Maven, cukup satu dependensi ber-scope test:
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>5.11.4</version>
<scope>test</scope>
</dependency>Gradle memakai konfigurasi serupa pada blok dependencies { testImplementation("org.junit.jupiter:junit-jupiter:5.11.4") }. Scope test memastikan pustaka ini tidak ikut terbawa ke artefak produksi.
Direktori Test Paralel dengan Kode Utama #
Proyek Java mengikuti konvensi direktori yang memisahkan kode produksi dari kode uji:
proyek/
├── src/
│ ├── main/java/id/example/layanan/PembersihTeks.java
│ └── test/java/id/example/layanan/PembersihTeksTest.java
└── pom.xmlPerhatikan dua hal. Pertama, berkas test tinggal di paket yang sama dengan class yang diuji (id.example.layanan), sehingga test dapat mengakses member ber-visibility paket tanpa membuka visibility publik. Kedua, nama berkas test adalah nama class ditambah akhiran Test — konvensi yang dibaca Maven Surefire dan Gradle untuk menemukan seluruh test secara otomatis.
Anatomi Test Jupiter #
Satu test Jupiter paling sederhana hanya butuh anotasi @Test di atas method void:
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
class PembersihTeksTest {
@Test
void rapikanMenyatukanSpasi() {
assertEquals("belajar junit", PembersihTeks.rapikan(" Belajar JUnit "));
}
@Test
@DisplayName("teks tiga kata dihitung sebagai tiga")
void hitungKataMenghitungTiga() {
assertEquals(3, PembersihTeks.hitungKata(" Belajar JUnit Jupiter "));
}
@Test
@Disabled("menunggu perbaikan bug JIRA-104")
void sementaraDilewati() {
// test ini tidak dijalankan runner
}
}Anotasi @DisplayName mengganti label test di laporan dengan kalimat yang terbaca seperti spesifikasi perilaku. Anotasi @Disabled menonaktifkan test sementara — misalnya saat fitur terkait sedang dikerjakan ulang — dan sebaiknya selalu disertai alasan. Kedua anotasi ini menggantikan peran nama method yang panjang dengan dokumentasi yang lebih manusiawi.
Test Bersarang dengan @Nested #
Saat satu class uji memuat banyak skenario, @Nested memungkinkan kita mengelompokkannya ke class dalam bernama:
class RekeningTest {
@Nested
class KetikaSaldoCukup {
@Test
void penarikanBerhasil() { /* ... */ }
}
@Nested
class KetikaSaldoKurang {
@Test
void penarikanDitolak() { /* ... */ }
}
}Runner mengeksekusi RekeningTest > KetikaSaldoCukup > penarikanBerhasil — hierarki yang membuat laporan panjang tetap mudah dipindai. Class dalam @Nested harus non-static; runner membuat instance barunya untuk tiap test.
Jupiter Mini dari Java Murni #
Bagaimana sebenarnya runner menemukan test bertanda @Test? Jawabannya persis seperti runner pada Chapter Testing Fundamentals, tetapi penyaringnya bukan awalan nama melainkan anotasi. Kita buat anotasi @Uji dengan @Retention(RetentionPolicy.RUNTIME) agar terbaca reflection saat program berjalan:
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Uji {
String nama() default "";
}Dua definisi pendukung menyusul: assertion mini Cek dan class produksi PembersihTeks:
import java.util.Objects;
public final class Cek {
private Cek() {
// kelas utilitas: tidak perlu instance
}
public static void sama(Object diharapkan, Object aktual) {
if (!Objects.equals(diharapkan, aktual)) {
throw new AssertionError("diharapkan '" + diharapkan
+ "' tetapi aktual '" + aktual + "'");
}
}
}public class PembersihTeks {
public static String rapikan(String masukan) {
if (masukan == null) {
return "";
}
return masukan.trim().toLowerCase().replaceAll("\\s+", " ");
}
public static int hitungKata(String masukan) {
String rapian = rapikan(masukan);
if (rapian.isEmpty()) {
return 0;
}
return rapian.split(" ").length;
}
}Class uji menandai method dengan @Uji dan memakai atribut nama sebagai padanan @DisplayName:
public class PembersihTeksTest {
@Uji
public void hitungKataUntukTeksKosong() {
Cek.sama(0, PembersihTeks.hitungKata(" "));
}
@Uji(nama = "teks tiga kata dihitung sebagai tiga")
public void hitungKataMenghitungTiga() {
Cek.sama(3, PembersihTeks.hitungKata(" Belajar JUnit Jupiter "));
}
@Uji
public void rapikanMenanganiNull() {
Cek.sama("", PembersihTeks.rapikan(null));
}
@Uji(nama = "teks dengan spasi ganda menjadi satu spasi")
public void rapikanMenyatukanSpasi() {
Cek.sama("belajar junit", PembersihTeks.rapikan(" Belajar JUnit "));
}
}Runner mini berikut menyalin inti mekanisme Jupiter: reflection untuk menemukan method ber-anotasi, pembacaan atribut anotasi sebagai label, dan eksekusi terisolasi:
import java.lang.reflect.Method;
import java.util.Arrays;
public class RunnerJupiterMini {
public static void main(String[] args) {
int lulus = 0;
int gagal = 0;
Method[] semua = PembersihTeksTest.class.getDeclaredMethods();
Arrays.sort(semua, (a, b) -> a.getName().compareTo(b.getName()));
for (Method method : semua) {
if (!method.isAnnotationPresent(Uji.class)) {
continue; // bukan method uji
}
String label = method.getAnnotation(Uji.class).nama();
if (label.isEmpty()) {
label = method.getName();
}
try {
method.invoke(new PembersihTeksTest());
lulus++;
System.out.println("LULUS [" + label + "]");
} catch (Exception e) {
gagal++;
Throwable sebab = e.getCause() != null ? e.getCause() : e;
System.out.println("GAGAL [" + label + "] -> " + sebab.getMessage());
}
}
System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
}
}Output yang Dihasilkan:
LULUS [teks tiga kata dihitung sebagai tiga]
LULUS [hitungKataUntukTeksKosong]
LULUS [rapikanMenanganiNull]
LULUS [teks dengan spasi ganda menjadi satu spasi]
ringkasan: 4 lulus, 0 gagalBedah baris-per-baris untuk bagian paling penting di RunnerJupiterMini:
- Baris
@Retention(RetentionPolicy.RUNTIME)(diUji.java): tanpa retention RUNTIME, compiler membuang anotasi setelah kompilasi danisAnnotationPresentselalu mengembalikanfalse— jebakan nomor satu saat membuat anotasi sendiri. - Baris
method.isAnnotationPresent(Uji.class): penyaring method uji berbasis penanda, bukan nama. Method pembantu apa pun tetap aman dieksekusi runner. - Baris
method.getAnnotation(Uji.class).nama(): atribut anotasi dibaca seperti method biasa; nilai kosong dialihkan ke nama method, meniru perilaku@DisplayNameyang opsional. - Baris
Arrays.sort(semua, ...): urutangetDeclaredMethods()tidak dijamin spesifikasi Java, sehingga pengurutan nama menjaga laporan tetap deterministik — kebiasaan yang sama dengan runner Chapter sebelumnya. - Baris
method.invoke(new PembersihTeksTest()): tiap test berjalan di instance segar; satu test yang melempar exception tidak menghalangi test berikutnya karena tangkapancatchmemisahkan kegagalannya.
Dari sini seluruh anotasi Jupiter lain — @BeforeEach, @ParameterizedTest, @TestFactory — hanyalah variasi pola yang sama: penanda yang dibaca runner, atribut yang menjadi konfigurasi, dan eksekusi yang terisolasi. Chapter berikutnya memperdalam alat utama setiap test: assertion beserta pesan kegagalannya.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
Cek.java
Unduh berkasimport java.util.Objects;
public final class Cek {
private Cek() {
// kelas utilitas: tidak perlu instance
}
public static void sama(Object diharapkan, Object aktual) {
if (!Objects.equals(diharapkan, aktual)) {
throw new AssertionError("diharapkan '" + diharapkan
+ "' tetapi aktual '" + aktual + "'");
}
}
}
PembersihTeks.java
Unduh berkaspublic class PembersihTeks {
public static String rapikan(String masukan) {
if (masukan == null) {
return "";
}
return masukan.trim().toLowerCase().replaceAll("\\s+", " ");
}
public static int hitungKata(String masukan) {
String rapian = rapikan(masukan);
if (rapian.isEmpty()) {
return 0;
}
return rapian.split(" ").length;
}
}
PembersihTeksTest.java
Unduh berkaspublic class PembersihTeksTest {
@Uji
public void hitungKataUntukTeksKosong() {
Cek.sama(0, PembersihTeks.hitungKata(" "));
}
@Uji(nama = "teks tiga kata dihitung sebagai tiga")
public void hitungKataMenghitungTiga() {
Cek.sama(3, PembersihTeks.hitungKata(" Belajar JUnit Jupiter "));
}
@Uji
public void rapikanMenanganiNull() {
Cek.sama("", PembersihTeks.rapikan(null));
}
@Uji(nama = "teks dengan spasi ganda menjadi satu spasi")
public void rapikanMenyatukanSpasi() {
Cek.sama("belajar junit", PembersihTeks.rapikan(" Belajar JUnit "));
}
}
RunnerJupiterMini.java
Unduh berkasimport java.lang.reflect.Method;
import java.util.Arrays;
public class RunnerJupiterMini {
public static void main(String[] args) {
int lulus = 0;
int gagal = 0;
Method[] semua = PembersihTeksTest.class.getDeclaredMethods();
Arrays.sort(semua, (a, b) -> a.getName().compareTo(b.getName()));
for (Method method : semua) {
if (!method.isAnnotationPresent(Uji.class)) {
continue; // bukan method uji
}
String label = method.getAnnotation(Uji.class).nama();
if (label.isEmpty()) {
label = method.getName();
}
try {
method.invoke(new PembersihTeksTest());
lulus++;
System.out.println("LULUS [" + label + "]");
} catch (Exception e) {
gagal++;
Throwable sebab = e.getCause() != null ? e.getCause() : e;
System.out.println("GAGAL [" + label + "] -> " + sebab.getMessage());
}
}
System.out.println("ringkasan: " + lulus + " lulus, " + gagal + " gagal");
}
}
Uji.java
Unduh berkasimport java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface Uji {
String nama() default "";
}