Menulis unit test yang benar untuk kode OOP dengan JUnit 5: anotasi Test dan BeforeEach, assertions, penamaan test yang ekspresif, lalu mocking dengan Mockito lewat when, mock dan verify serta konsep test doubles fake, stub dan mock.

Semua skill OOP yang kalian bangun dari episode 2 sampai 15 menghasilkan satu pembeda utama di dunia kerja: kode yang bisa ditest. Episode ini mengajarkan caranya — menulis unit test dengan JUnit 5 dan Mockito untuk memverifikasi behavior object tanpa perlu menjalankan seluruh aplikasi.
Mengapa testing adalah puncak dari OOP? Karena design yang bagus (SOLID, DI) membuat kode mudah ditest, dan testing adalah umpan balik yang membuat desain makin baik. Kalau kalian kesulitan menulis test, biasanya itu gejala desain yang buruk — bukan kesalahan test-nya. Episode ini menutup lingkaran: prinsip di episode 6-15 membayar dirinya sendiri di sini.
Untuk pertama kalinya di series ini kita butuh build tool. Buat proyek Maven sederhana:
mkdir -p src/main/java src/test/javaDengan Maven, dependensi JUnit ditambahkan di pom.xml. Jalankan test dengan:
mvn testIntelliJ IDEA dan VS Code juga menyediakan tombol ▶ untuk menjalankan test individual — bekerja sama dengan JUnit secara otomatis.
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class KalkulatorTest {
@Test
void tambahDuaAngkaMengembalikanJumlahnya() {
Kalkulator kalkulator = new Kalkulator();
int hasil = kalkulator.tambah(2, 3);
assertEquals(5, hasil);
}
}Struktur test yang baik mengikuti pola arrange-act-assert:
new Kalkulator()).kalkulator.tambah(2, 3)).assertEquals(5, hasil)).Nama test dalam bahasa Indonesia yang deskriptif (tambahDuaAngkaMengembalikanJumlahnya) jauh lebih berharga daripada testTambah — saat test gagal, nama menjelaskan kontrak yang dilanggar.
| Assertion | Memverifikasi |
|---|---|
assertEquals(a, b) | Kesetaraan nilai |
assertTrue/assertFalse | Kondisi boolean |
assertNull/assertNotNull | Nullability |
assertThrows(...) | Exception dilempar |
assertIterableEquals(...) | Kesetaraan koleksi berurutan |
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class RekeningTest {
@Test
void depositNegatifMenolakDenganException() {
Rekening rekening = new Rekening("Alya", 0);
IllegalArgumentException err = assertThrows(
IllegalArgumentException.class,
() -> rekening.deposit(-100));
assertEquals("Nominal harus positif", err.getMessage());
}
}Perhatikan: test ini memverifikasi invarian yang kita bangun di episode 6 — setter/constructor menolak nilai tidak valid. Inilah kenapa encapsulation menghasilkan kode yang mudah diuji: aturan domain berada di satu tempat yang jelas.
Saat setiap test butuh setup yang sama, gunakan @BeforeEach — dieksekusi sebelum setiap test (bukan sekali per class):
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class KeranjangTest {
private Keranjang keranjang;
@BeforeEach
void setUp() {
keranjang = new Keranjang();
}
@Test
void keranjangKosongSaatBaruDibuat() {
assertEquals(0, keranjang.total());
}
@Test
void tambahSatuItemMenambahTotal() {
keranjang.tambah(new Item("Kopi", 25000));
assertEquals(25000, keranjang.total());
}
}Setiap test mendapatkan instance Keranjang baru — tidak ada state yang bocor antar test. Inilah prinsip isolasi: satu test tidak boleh bergantung pada test lain.
Kode OOP yang baik (DIP, episode 14) membuat class menerima dependency lewat constructor. Tapi saat diuji, dependency itu bisa berbahaya — mengirim email asli, memanggil server, atau menulis ke database:
public class Layanan {
private final Pengirim pengirim;
public Layanan(Pengirim pengirim) {
this.pengirim = pengirim;
}
public void notif(String pesan) {
pengirim.kirim(pesan);
}
}Kita tidak ingin notif benar-benar mengirim email saat test. Solusinya: test doubles — pengganti yang aman dan terkontrol.
| Jenis | Peran |
|---|---|
| Fake | Implementasi sederhana yang bekerja (misal repository di memory) |
| Stub | Mengembalikan jawaban tetap yang sudah diatur |
| Mock | Merekam interaksi + jawaban yang diatur; bisa diverifikasi |
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.mockito.Mockito.*;
public class LayananTest {
@Test
void notifMengirimPesanKePengirim() {
Pengirim pengirim = mock(Pengirim.class);
Layanan layanan = new Layanan(pengirim);
layanan.notif("Halo");
verify(pengirim).kirim("Halo");
}
@Test
void kirimMengembalikanStatusBerdasarkanStub() {
Pengirim pengirim = mock(Pengirim.class);
when(pengirim.kirim(anyString())).thenReturn(true);
Layanan layanan = new Layanan(pengirim);
boolean sukses = layanan.kirim("Pesan");
assertTrue(sukses);
}
}Tiga operasi inti Mockito:
mock(Class) — membuat object palsu tanpa implementasi.when(...).thenReturn(...) — mengatur jawaban (stubbing).verify(...) — memastikan method dipanggil (verifikasi interaksi).Keindahannya: Layanan yang diuji adalah object asli; hanya dependency-nya yang diganti. Inilah test yang memverifikasi behavior kita, bukan behavior dependency kita — dan inilah alasan DIP (episode 14) begitu berharga: dependency yang di-inject mudah diganti mock.
Tip
Jangan meng-mock terlalu banyak. Mock dependency yang berbahaya (I/O, jaringan) itu wajar; tetapi jika semuanya di-mock termasuk data sendiri, test hanya menguji mock melawan mock. Sebaiknya test 1-2 lapisan nyata, dan mock hanya di perbatasan.
Thread.sleep atau nilai yang berubah; buat deterministik.Warning
Jangan pernah menulis test yang selalu lolos tanpa menguji apa pun — misal tanpa assertion, atau membandingkan hasil dengan dirinya sendiri. Test seperti itu memberi rasa aman palsu dan menahan kode rusak masuk ke produksi. Test yang baik harus bisa gagal ketika behavior benar-benar rusak.
Istilah code coverage (persentase baris yang dieksekusi test) sering disalahartikan. Coverage 90% bukan jaminan kualitas — yang penting adalah behavior penting teruji. Fokuskan energi pada:
Coverage adalah alat bantu menemukan kode yang belum diuji — bukan angka yang harus dikejar sampai maksimal.
Inti yang harus dibawa pulang:
@Test, @BeforeEach, assertions, dan nama test yang ekspresif dengan pola arrange-act-assert.mock, when(...).thenReturn(...), verify(...).Di episode 17 selanjutnya kita akan membahas concurrency & threading — Thread, Runnable, synchronized, volatile, Executors, lalu virtual threads dan structured concurrency untuk Java modern. Sampai jumpa di episode 17!