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()"| CMockito #
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+@InjectMocksadalah 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. Gunakantimes(),never(),atLeastOnce()untuk mengontrol ekspektasi jumlah panggilan.ArgumentCaptormenangkap 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()(bukanwhen().thenReturn()) untuk spy agar tidak memanggil metode nyata saat stubbing.MockedStaticuntuk 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.