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:

JenisPerilaku DefaultKapan Dipakai
MockSemua metode return nilai default (null, 0, false) sampai di-stubKetika perlu kontrol penuh dan verifikasi interaksi
StubObjek sederhana yang mengembalikan nilai tetapKetika hanya perlu kontrol input, tidak peduli interaksi
SpyMembungkus objek nyata, metode yang tidak di-stub memanggil implementasi asliKetika 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 #

AspekMockitoEasyMock
Gaya stubbingwhen(mock.method()).thenReturn(value)expect(mock.method()).andReturn(value)
AktivasiTidak perlu (langsung aktif)Wajib panggil replay()
Verifikasiverify(mock).method()verify(mock) — semua ekspektasi
Default perilakuReturn null/0/falseThrow jika dipanggil tanpa ekspektasi
PopularitasSangat tinggiLebih 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