Mocking

Mocking #

Unit test yang baik berjalan cepat dan terisolasi — tidak menyentuh database, tidak memanggil API eksternal, tidak bergantung pada jam sistem atau variabel lingkungan. Tapi bagaimana cara menguji ProdukService yang memanggil ProdukRepository untuk mengambil data dari database? Jawabannya: ganti ProdukRepository nyata dengan objek tiruan (mock) yang kamu kontrol sepenuhnya. Kamu tentukan: “jika findById(1) dipanggil, kembalikan produk ini.” Kode yang diuji tidak tahu bedanya — ia tetap berinteraksi dengan interface yang sama. Dengan mocking, kamu bisa menguji setiap cabang logika ProdukService tanpa menyalakan satu pun database. Di Java, Mockito adalah library mocking yang paling banyak dipakai — ekspresif, terintegrasi baik dengan JUnit 5, dan mendukung hampir semua skenario yang kamu butuhkan.

Konsep Dasar #

Sebelum menulis kode, ada tiga jenis objek tiruan yang sering tertukar penggunaannya:

Jenis Perilaku Default Kapan Dipakai
Mock Semua metode return nilai default (null, 0, false) sampai di-stub Ketika perlu kontrol penuh dan verifikasi interaksi
Stub Objek sederhana yang mengembalikan nilai tetap Ketika hanya perlu kontrol input, tidak peduli interaksi
Spy Membungkus objek nyata, metode yang tidak di-stub memanggil implementasi asli Ketika ingin override sebagian perilaku objek yang sudah ada
flowchart LR
    A["Test"] -->|"memanggil"| B["Kelas yang diuji\n(ProdukService)"]
    B -->|"memanggil"| C["Mock/Stub/Spy\n(ProdukRepository palsu)"]
    C -->|"return nilai yang\nsudah ditentukan"| B
    B -->|"return hasil"| A
    A -->|"verify()"| C

Mockito #

Mockito adalah library mocking standar de facto di ekosistem Java. Ia bekerja dengan membuat subkelas atau proxy dari kelas/interface yang di-mock menggunakan bytecode manipulation.

Dependensi #

<!-- Maven — mockito-junit-jupiter sudah mencakup mockito-core -->
<dependency>
    <groupId>org.mockito</groupId>
    <artifactId>mockito-junit-jupiter</artifactId>
    <version>5.11.0</version>
    <scope>test</scope>
</dependency>
// Gradle
testImplementation 'org.mockito:mockito-junit-jupiter:5.11.0'

Membuat Mock — Dua Cara #

import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.Mock;
import org.mockito.InjectMocks;
import org.mockito.junit.jupiter.MockitoExtension;

import static org.mockito.Mockito.*;
import static org.junit.jupiter.api.Assertions.*;

// Interface dan kelas yang diuji
interface ProdukRepository {
    Produk findById(Long id);
    List<Produk> findAll();
    Produk save(Produk produk);
    void delete(Long id);
}

class ProdukService {
    private final ProdukRepository repository;

    public ProdukService(ProdukRepository repository) {
        this.repository = repository;
    }

    public Produk getProduk(Long id) {
        Produk p = repository.findById(id);
        if (p == null) throw new RuntimeException("Produk tidak ditemukan: " + id);
        return p;
    }

    public Produk buatProduk(String nama, double harga) {
        if (nama == null || nama.isBlank()) throw new IllegalArgumentException("Nama wajib diisi");
        Produk baru = new Produk(null, nama, harga);
        return repository.save(baru);
    }
}

// Cara 1: @ExtendWith + @Mock (direkomendasikan)
@ExtendWith(MockitoExtension.class)
class ProdukServiceTest {

    @Mock
    ProdukRepository repository;    // Mockito buat mock otomatis

    @InjectMocks
    ProdukService service;          // Mockito inject mock ke konstruktor/field

    @Test
    void getProduk_returnsExistingProduct() {
        // Arrange — tentukan perilaku mock
        Produk laptop = new Produk(1L, "Laptop", 12_000_000);
        when(repository.findById(1L)).thenReturn(laptop);

        // Act
        Produk hasil = service.getProduk(1L);

        // Assert
        assertEquals("Laptop", hasil.nama());
        assertEquals(12_000_000, hasil.harga());
    }
}

// Cara 2: Mockito.mock() manual (untuk test tanpa @ExtendWith)
class ProdukServiceTestManual {

    @Test
    void getProdukManual() {
        ProdukRepository mockRepo = mock(ProdukRepository.class);
        ProdukService service = new ProdukService(mockRepo);

        when(mockRepo.findById(1L)).thenReturn(new Produk(1L, "Mouse", 150_000));
        assertEquals("Mouse", service.getProduk(1L).nama());
    }
}

Stubbing — Menentukan Perilaku Mock #

@ExtendWith(MockitoExtension.class)
class StubbingTest {

    @Mock ProdukRepository repository;

    @Test
    void berbagaiCaraStubbing() {
        Produk laptop = new Produk(1L, "Laptop", 12_000_000);

        // thenReturn: kembalikan nilai tetap
        when(repository.findById(1L)).thenReturn(laptop);

        // thenReturn dengan beberapa nilai: pertama kali return laptop, berikutnya null
        when(repository.findById(2L))
            .thenReturn(laptop)
            .thenReturn(null);

        // thenThrow: lempar exception
        when(repository.findById(999L))
            .thenThrow(new RuntimeException("Koneksi database gagal"));

        // thenAnswer: logika kustom berdasarkan argumen
        when(repository.findById(anyLong()))
            .thenAnswer(invocation -> {
                Long id = invocation.getArgument(0);
                return id > 0 ? new Produk(id, "Produk-" + id, id * 1000.0) : null;
            });

        // doReturn (untuk void method atau spy)
        doNothing().when(repository).delete(anyLong());
        doThrow(new RuntimeException("Gagal hapus")).when(repository).delete(-1L);

        // Stubbing untuk void method
        // (tidak bisa pakai when().thenXxx() untuk void)
        doAnswer(invocation -> {
            System.out.println("Menghapus ID: " + invocation.getArgument(0));
            return null;
        }).when(repository).delete(anyLong());
    }
}

Argument Matchers #

Argument matchers memungkinkan stubbing dan verifikasi yang lebih fleksibel — kamu tidak perlu menentukan nilai argumen secara eksak.

import static org.mockito.ArgumentMatchers.*;

@Test
void argumentMatchers() {
    Produk laptop = new Produk(1L, "Laptop", 12_000_000);

    // any(): cocok dengan argumen apa pun dari tipe tertentu
    when(repository.findById(any(Long.class))).thenReturn(laptop);
    when(repository.findById(anyLong())).thenReturn(laptop);   // shorthand

    // eq(): cocok dengan nilai eksak (berguna saat kombinasi dengan matcher lain)
    when(repository.findById(eq(1L))).thenReturn(laptop);

    // Matcher untuk string
    when(repository.findByNama(anyString())).thenReturn(List.of(laptop));
    when(repository.findByNama(startsWith("Lap"))).thenReturn(List.of(laptop));
    when(repository.findByNama(contains("top"))).thenReturn(List.of(laptop));

    // Matcher untuk koleksi
    when(repository.findAllById(anyList())).thenReturn(List.of(laptop));

    // isNull() dan isNotNull()
    when(repository.save(isNull())).thenThrow(new IllegalArgumentException());
    when(repository.save(isNotNull())).thenReturn(laptop);

    // Kombinasi — PERHATIAN: jika pakai matcher di satu argumen,
    // SEMUA argumen harus pakai matcher
    // when(repo.findByNamaAndHarga("Laptop", anyDouble())).thenReturn(...); // ✗ error
    when(repository.findByNamaAndHarga(eq("Laptop"), anyDouble())).thenReturn(List.of(laptop)); // ✓
}

Verifikasi Interaksi #

Setelah menjalankan kode yang diuji, kamu bisa memverifikasi bagaimana mock dipanggil — berapa kali, dengan argumen apa, dalam urutan apa.

Verifikasi Dasar #

@Test
void verifikasiInteraksi() {
    Produk laptop = new Produk(1L, "Laptop", 12_000_000);
    when(repository.findById(1L)).thenReturn(laptop);

    service.getProduk(1L);

    // Verifikasi dipanggil tepat sekali
    verify(repository).findById(1L);
    verify(repository, times(1)).findById(1L);     // eksplisit

    // Verifikasi jumlah panggilan
    verify(repository, times(3)).findById(anyLong()); // dipanggil 3 kali
    verify(repository, atLeastOnce()).findById(1L);    // minimal sekali
    verify(repository, atLeast(2)).findById(anyLong()); // minimal 2 kali
    verify(repository, atMost(5)).findById(anyLong());  // maksimal 5 kali
    verify(repository, never()).delete(anyLong());       // tidak pernah dipanggil

    // Verifikasi tidak ada interaksi lain selain yang sudah diverifikasi
    verifyNoMoreInteractions(repository);

    // Verifikasi sama sekali tidak ada interaksi
    verifyNoInteractions(repository);
}

Verifikasi Urutan Panggilan #

import org.mockito.InOrder;

@Test
void verifikasiUrutan() {
    when(repository.findById(1L)).thenReturn(new Produk(1L, "Laptop", 12_000_000));

    // Jalankan beberapa operasi
    service.getProduk(1L);
    service.getProduk(1L);

    // Pastikan dipanggil dalam urutan ini
    InOrder urutan = inOrder(repository);
    urutan.verify(repository).findById(1L);
    urutan.verify(repository).findById(1L);
}

ArgumentCaptor — Tangkap Argumen #

ArgumentCaptor sangat berguna saat kamu perlu memverifikasi nilai yang dikirim ke mock, terutama saat objek dibuat di dalam metode yang diuji dan kamu tidak bisa mengaksesnya langsung.

import org.mockito.ArgumentCaptor;
import org.mockito.Captor;

@ExtendWith(MockitoExtension.class)
class CaptorTest {

    @Mock ProdukRepository repository;
    @InjectMocks ProdukService service;

    @Captor ArgumentCaptor<Produk> produkCaptor;

    @Test
    void buatProduk_menyimpanDenganDataYangBenar() {
        Produk tersimpan = new Produk(1L, "Monitor", 3_500_000);
        when(repository.save(any())).thenReturn(tersimpan);

        service.buatProduk("Monitor", 3_500_000);

        // Tangkap argumen yang dikirim ke repository.save()
        verify(repository).save(produkCaptor.capture());
        Produk yangDikirim = produkCaptor.getValue();

        // Verifikasi isi objek yang dikirim
        assertEquals("Monitor", yangDikirim.nama());
        assertEquals(3_500_000, yangDikirim.harga());
        assertNull(yangDikirim.id()); // belum dapat ID dari DB
    }

    @Test
    void simpanBanyakProduk_semuaTercapture() {
        when(repository.save(any())).thenAnswer(i -> i.getArgument(0));

        service.buatProduk("Produk A", 1000);
        service.buatProduk("Produk B", 2000);
        service.buatProduk("Produk C", 3000);

        verify(repository, times(3)).save(produkCaptor.capture());
        List<Produk> semua = produkCaptor.getAllValues();

        assertEquals(3, semua.size());
        assertEquals("Produk A", semua.get(0).nama());
        assertEquals("Produk C", semua.get(2).nama());
    }
}

Spy — Membungkus Objek Nyata #

Spy cocok saat kamu ingin menguji sebagian besar perilaku nyata sebuah objek, tapi perlu meng-override beberapa metode tertentu.

Membuat dan Menggunakan Spy #

import org.mockito.Spy;

@ExtendWith(MockitoExtension.class)
class SpyTest {

    // Spy membungkus objek nyata (berbeda dari mock yang murni palsu)
    @Spy
    List<String> daftar = new ArrayList<>();

    @Test
    void spyMenggunakanImplementasiAsli() {
        // Metode yang tidak di-stub dipanggil seperti biasa
        daftar.add("Apel");
        daftar.add("Mangga");

        assertEquals(2, daftar.size()); // ukuran nyata

        // Stub hanya metode tertentu
        doReturn(100).when(daftar).size();
        assertEquals(100, daftar.size()); // sekarang return 100

        // Tapi isi daftar masih nyata
        assertTrue(daftar.contains("Apel"));
    }

    @Test
    void spyDariObjekYangAda() {
        // Cara membuat spy dari objek yang sudah ada
        EmailService emailService = spy(new EmailServiceImpl());

        // Override hanya metode kirim agar tidak benar-benar kirim email
        doNothing().when(emailService).kirimEmail(anyString(), anyString());

        // Metode lain (seperti validasi) masih berjalan nyata
        assertTrue(emailService.emailValid("[email protected]"));

        // Panggil metode yang menggunakan emailService
        NotifikasiService notif = new NotifikasiService(emailService);
        notif.kirimNotifikasiDaftar("[email protected]");

        // Verifikasi kirimEmail dipanggil dengan argumen yang benar
        verify(emailService).kirimEmail(eq("[email protected]"), contains("Selamat datang"));
    }
}

Perhatian Saat Stubbing Spy #

@Test
void perhatianStubbingSpy() {
    List<String> list = spy(new ArrayList<>());

    // ANTI-PATTERN: when().thenReturn() pada spy memanggil metode nyata dulu
    // List kosong → get(0) melempar IndexOutOfBoundsException sebelum thenReturn dijalankan!
    // when(list.get(0)).thenReturn("nol"); // ✗ lempar exception

    // BENAR: gunakan doReturn().when() untuk spy — tidak memanggil metode nyata
    doReturn("nol").when(list).get(0); // ✓ aman
}

Mock Static Method #

Sejak Mockito 3.4.0+, method static bisa di-mock menggunakan MockedStatic. Ini berguna untuk mocking LocalDate.now(), UUID.randomUUID(), atau utility class lainnya.

Mocking Metode Static #

import org.mockito.MockedStatic;
import java.time.LocalDate;

@Test
void mockTanggalHariIni() {
    LocalDate tanggalTetap = LocalDate.of(2025, 8, 17);

    // try-with-resources: mock static hanya aktif dalam blok ini
    try (MockedStatic<LocalDate> mockedDate = mockStatic(LocalDate.class)) {
        mockedDate.when(LocalDate::now).thenReturn(tanggalTetap);

        // Kode yang memanggil LocalDate.now() di dalam blok ini
        // akan mendapat 2025-08-17, bukan tanggal sebenarnya
        LocalDate hasil = LocalDate.now();
        assertEquals(LocalDate.of(2025, 8, 17), hasil);

        // Kode yang diuji juga terdampak
        String laporan = service.buatLaporan(); // di dalam buatLaporan() ada LocalDate.now()
        assertTrue(laporan.contains("2025-08-17"));
    }

    // Di luar blok try: LocalDate.now() kembali normal
}

@Test
void mockUUID() {
    java.util.UUID tetap = java.util.UUID.fromString("123e4567-e89b-12d3-a456-426614174000");

    try (MockedStatic<java.util.UUID> mockedUUID = mockStatic(java.util.UUID.class)) {
        mockedUUID.when(java.util.UUID::randomUUID).thenReturn(tetap);

        String id = service.buatIdUnik();
        assertEquals("123e4567-e89b-12d3-a456-426614174000", id);
    }
}

EasyMock #

EasyMock adalah alternatif Mockito dengan pendekatan yang sedikit berbeda — ia menggunakan pola record-replay-verify. Kamu rekam ekspektasi dulu, aktifkan dengan replay(), jalankan kode, lalu verifikasi dengan verify().

Dependensi dan Penggunaan #

<!-- Maven -->
<dependency>
    <groupId>org.easymock</groupId>
    <artifactId>easymock</artifactId>
    <version>5.2.0</version>
    <scope>test</scope>
</dependency>
import org.easymock.EasyMock;
import static org.easymock.EasyMock.*;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;

class ProdukServiceEasyMockTest {

    @Test
    void getProdukDenganEasyMock() {
        // 1. Buat mock
        ProdukRepository mockRepo = createMock(ProdukRepository.class);

        // 2. Record: tentukan ekspektasi
        Produk laptop = new Produk(1L, "Laptop", 12_000_000);
        expect(mockRepo.findById(1L)).andReturn(laptop);

        // 3. Replay: aktifkan mock
        replay(mockRepo);

        // 4. Jalankan kode yang diuji
        ProdukService service = new ProdukService(mockRepo);
        Produk hasil = service.getProduk(1L);

        // 5. Assert
        assertEquals("Laptop", hasil.nama());

        // 6. Verify: pastikan semua ekspektasi terpenuhi
        verify(mockRepo);
    }

    @Test
    void mockLemparException() {
        ProdukRepository mockRepo = createMock(ProdukRepository.class);

        // Ekspektasi: findById(999) lempar exception
        expect(mockRepo.findById(999L))
            .andThrow(new RuntimeException("Tidak ditemukan"));
        replay(mockRepo);

        ProdukService service = new ProdukService(mockRepo);
        assertThrows(RuntimeException.class, () -> service.getProduk(999L));

        verify(mockRepo);
    }
}

Perbedaan EasyMock vs Mockito #

Aspek Mockito EasyMock
Gaya stubbing when(mock.method()).thenReturn(value) expect(mock.method()).andReturn(value)
Aktivasi Tidak perlu (langsung aktif) Wajib panggil replay()
Verifikasi verify(mock).method() verify(mock) — semua ekspektasi
Default perilaku Return null/0/false Throw jika dipanggil tanpa ekspektasi
Popularitas Sangat tinggi Lebih rendah, kurang aktif dikembangkan

Anti-Pattern Mocking #

Over-mocking #

// ANTI-PATTERN: mock terlalu banyak hal, test tidak menguji logika nyata
@Test
void testTerlalubanyakMock() {
    when(repository.findById(any())).thenReturn(mock(Produk.class));
    when(produk.getNama()).thenReturn("Laptop");
    when(produk.getHarga()).thenReturn(12_000_000.0);
    when(formatter.format(any())).thenReturn("Rp12.000.000");
    // ... 10 stub lagi
    // Test ini hanya menguji bahwa mock dipanggil, bukan logika bisnis

// BENAR: mock hanya dependensi eksternal (I/O, network, DB)
// Gunakan objek nyata untuk value objects dan logika internal
@Test
void testYangBenar() {
    Produk laptop = new Produk(1L, "Laptop", 12_000_000); // objek nyata
    when(repository.findById(1L)).thenReturn(laptop);      // hanya DB yang di-mock
    // ...
}

Menguji Detail Implementasi, Bukan Perilaku #

// ANTI-PATTERN: verifikasi cara implementasi bekerja, bukan hasilnya
@Test
void testImplementasiDetail() {
    service.getProduk(1L);

    // Ini menguji "bagaimana" bukan "apa" — fragile jika implementasi berubah
    verify(repository).findById(1L);
    verify(cache).get("produk:1");
    verify(logger).log(any());
    // test ini akan gagal jika kita refactor urutan panggilan internal
}

// BENAR: fokus pada perilaku yang terlihat dari luar
@Test
void testPerilaku() {
    Produk laptop = new Produk(1L, "Laptop", 12_000_000);
    when(repository.findById(1L)).thenReturn(laptop);

    Produk hasil = service.getProduk(1L); // verifikasi hasil, bukan cara mencapainya

    assertEquals("Laptop", hasil.nama());
    // hanya verify yang benar-benar penting secara kontrak
}

Mock Kelas yang Tidak Kamu Miliki #

// ANTI-PATTERN: mock library pihak ketiga langsung
HttpClient mockClient = mock(HttpClient.class);
when(mockClient.send(any(), any())).thenReturn(mock(HttpResponse.class));
// Ini rapuh: API HttpClient bisa berubah, dan kamu sedang menguji asumsimu, bukan kode

// BENAR: bungkus library pihak ketiga dalam interface milikmu
interface HttpGateway {
    String get(String url);
    String post(String url, String body);
}

// Lalu mock interface yang kamu buat
HttpGateway mockGateway = mock(HttpGateway.class);
when(mockGateway.get("https://api.example.com/produk/1")).thenReturn("{\"id\":1}");

Kapan Menggunakan Mocking #

MOCK dependensi yang:
  ✓ Mengakses database atau penyimpanan eksternal
  ✓ Memanggil API atau layanan jaringan
  ✓ Bergantung pada waktu (LocalDate.now(), Instant.now())
  ✓ Mengakses sistem file atau variabel lingkungan
  ✓ Mahal untuk diinisialisasi di test
  ✓ Non-deterministik (random, threading)

JANGAN mock:
  ✗ Value objects (Produk, Alamat, Money) — pakai objek nyata
  ✗ Logika bisnis sederhana — pakai implementasi nyata
  ✗ Library pihak ketiga langsung — bungkus dulu dalam interface
  ✗ Kelas yang kamu sendiri tidak kontrol APInya

Pilih jenis mock:
  → @Mock + @InjectMocks  : paling umum, untuk interface dan kelas dengan injection
  → spy()                 : saat perlu override sebagian perilaku objek nyata
  → MockedStatic          : untuk static method (UUID, LocalDate, dll.)
  → ArgumentCaptor        : saat perlu verifikasi isi objek yang dikirim ke mock

Ringkasan #

  • Mock mengisolasi kode yang diuji dari dependensi eksternal — database, jaringan, sistem file diganti dengan objek yang kamu kontrol sepenuhnya, membuat test cepat dan deterministik.
  • @ExtendWith(MockitoExtension.class) + @Mock + @InjectMocks adalah pola standar Mockito dengan JUnit 5. Mockito otomatis membuat mock dan menyuntikkannya ke kelas yang diuji.
  • when().thenReturn() untuk stubbing nilai kembali, when().thenThrow() untuk mensimulasikan error, doNothing().when() untuk void method.
  • Argument matchers (any(), anyString(), eq(), contains()) membuat stubbing dan verifikasi lebih fleksibel saat argumen eksak tidak penting atau tidak diketahui.
  • verify(mock).metode() untuk memastikan interaksi terjadi. Gunakan times(), never(), atLeastOnce() untuk mengontrol ekspektasi jumlah panggilan.
  • ArgumentCaptor menangkap argumen yang dikirim ke mock — berguna saat objek dibuat di dalam metode yang diuji dan kamu perlu verifikasi isinya.
  • Spy membungkus objek nyata — metode yang tidak di-stub memanggil implementasi asli. Gunakan doReturn().when() (bukan when().thenReturn()) untuk spy agar tidak memanggil metode nyata saat stubbing.
  • MockedStatic untuk mock method static dalam blok try-with-resources — mock hanya aktif dalam blok tersebut dan otomatis dibatalkan setelahnya.
  • Jangan over-mock — mock hanya dependensi eksternal. Value objects, logika bisnis sederhana, dan objek yang tidak punya efek samping tidak perlu di-mock.

← Sebelumnya: Unit Test   Berikutnya: JSON →

About | Author | Content Scope | Editorial Policy | Privacy Policy | Disclaimer | Contact