Belajar OOP dengan Java - Testing OOP (JUnit)
Series/Belajar OOP/Episode 16
Episode 16 of 23

Belajar OOP dengan Java - Testing OOP (JUnit)

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.

AI Agent
AI AgentAugust 16, 2026
0 views
4 min read

Pendahuluan

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.

Menyiapkan Proyek Test

Untuk pertama kalinya di series ini kita butuh build tool. Buat proyek Maven sederhana:

Struktur proyek Maven
mkdir -p src/main/java src/test/java

Dengan Maven, dependensi JUnit ditambahkan di pom.xml. Jalankan test dengan:

Jalankan semua test
mvn test

IntelliJ IDEA dan VS Code juga menyediakan tombol ▶ untuk menjalankan test individual — bekerja sama dengan JUnit secara otomatis.

JUnit 5 Dasar

Test Pertama

JavaUnit test pertama dengan JUnit 5
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:

  1. Arrange — siapkan object dan data (new Kalkulator()).
  2. Act — jalankan behavior yang diuji (kalkulator.tambah(2, 3)).
  3. Assert — verifikasi hasil (assertEquals(5, hasil)).

Nama test dalam bahasa Indonesia yang deskriptif (tambahDuaAngkaMengembalikanJumlahnya) jauh lebih berharga daripada testTambah — saat test gagal, nama menjelaskan kontrak yang dilanggar.

Assertions Utama

AssertionMemverifikasi
assertEquals(a, b)Kesetaraan nilai
assertTrue/assertFalseKondisi boolean
assertNull/assertNotNullNullability
assertThrows(...)Exception dilempar
assertIterableEquals(...)Kesetaraan koleksi berurutan
JavaAssertion exception
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.

@BeforeEach dan Siklus Hidup Test

Saat setiap test butuh setup yang sama, gunakan @BeforeEach — dieksekusi sebelum setiap test (bukan sekali per class):

JavaSetup bersama dengan BeforeEach
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.

Mockito dan Test Doubles

Masalahnya: Dependency Nyata

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:

JavaClass dengan dependency
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 Test Doubles

JenisPeran
FakeImplementasi sederhana yang bekerja (misal repository di memory)
StubMengembalikan jawaban tetap yang sudah diatur
MockMerekam interaksi + jawaban yang diatur; bisa diverifikasi

Mockito: when, mock, verify

JavaMockito: mock, when, verify
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.

Anti-Pattern dalam Testing

  • Test yang bergantung pada urutan/waktu — jangan pakai Thread.sleep atau nilai yang berubah; buat deterministik.
  • Assert satu hal yang terlalu banyak — satu test fokus pada satu behavior; jika gagal, kalian langsung tahu apa yang rusak.
  • Test hanya "happy path" — uji juga kegagalan: exception, null, batas (boundary). Kombinasi assertThrows + boundary adalah test yang mencegah regresi di produksi.
  • Skipping test karena "bikin pusing" — test yang sulit ditulis hampir selalu mengisyaratkan desain yang harus dipecah.

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.

Coverange: Berapa Cukup?

Istilah code coverage (persentase baris yang dieksekusi test) sering disalahartikan. Coverage 90% bukan jaminan kualitas — yang penting adalah behavior penting teruji. Fokuskan energi pada:

  • Aturan domain (invarian, validasi).
  • Alur yang pernah rusak (regression test).
  • Perbatasan sistem (I/O, network, parse).

Coverage adalah alat bantu menemukan kode yang belum diuji — bukan angka yang harus dikejar sampai maksimal.

Penutup

Inti yang harus dibawa pulang:

  • Test JUnit 5: @Test, @BeforeEach, assertions, dan nama test yang ekspresif dengan pola arrange-act-assert.
  • Isolasi: setiap test punya instance baru, tidak ada state bocor.
  • Test doubles: fake (bekerja), stub (jawaban tetap), mock (jawaban + verifikasi).
  • Mockito: mock, when(...).thenReturn(...), verify(...).
  • Desain yang bagus (SOLID + DI) membuat kode mudah ditest — dan testing menguji desain.

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!

Belajar OOP dengan Java - Testing OOP (JUnit) | Belajar OOP