Chapter sebelumnya membangun stub buatan tangan dan menunjukkan konsep memutus dependensi eksternal. Menulis class pengganti manual untuk setiap interface, menandai jawaban, dan memeriksa rekaman panggilan adalah pekerjaan yang berulang persis sama di setiap test — dan pekerjaan yang berulang adalah kandidat otomasi. Mockito adalah framework de facto untuk otomasi tersebut: satu panggilan membuat implementasi buatan dari interface apa pun, beberapa baris memasang jawaban, dan satu statement memverifikasi bahwa interaksi yang diharapkan benar-benar terjadi. Pada Chapter ini kita akan memakai Mockito dalam gaya BDD — given/willReturn, verify, matcher seperti anyString, dan ArgumentCaptor — lalu membangun padanan manualnya dari Java murni agar konsep verifikasi interaksi terlihat telanjang.
Menambahkan Mockito ke Proyek #
Mockito ditambahkan sebagai dependensi test, biasanya bersama integrasi Jupiter-nya:
<dependency>
<groupId>org.mockito</groupId>
<artifactId>mockito-junit-jupiter</artifactId>
<version>5.15.2</version>
<scope>test</scope>
</dependency>Artefak mockito-junit-jupiter otomatis membawa mockito-core. Dengan ekstensi ini, field mock dapat dibuat langsung lewat anotasi:
@ExtendWith(MockitoExtension.class)
class KasirTest {
@Mock
GatewayPembayaran pembayaran;
@InjectMocks
Kasir kasir;
}Anotasi @Mock meminta Mockito membuat mock untuk interface GatewayPembayaran, sedangkan @InjectMocks membuat instance Kasir dan menyuntikkan mock ke constructornya.
Membuat Mock dan Memasang Jawaban #
Kode lengkap satu test Mockito bergaya BDD terlihat seperti ini:
import static org.mockito.BDDMockito.*;
import static org.assertj.core.api.Assertions.assertThat;
@Test
void pesananBerhasilMenghasilkanStruk() {
// given: jawaban gateway disiapkan
given(pembayaran.bayar("PSN-1", 250_000L))
.willReturn(new HasilBayar(true, "OK-42"));
// when: perilaku yang diuji
String struk = kasir.proses(new Pesanan("PSN-1", 250_000L));
// then: hasil dan interaksi diverifikasi
assertThat(struk).isEqualTo("STRUK-PSN-1");
verify(pembayaran).bayar("PSN-1", 250_000L);
}Di balik layar, Mockito memakai dynamic proxy (untuk interface) atau subclass yang dihasilkan saat runtime (untuk class) untuk membuat implementasi GatewayPembayaran yang mencatat setiap panggilan method beserta argumennya. Ketika given(...) dijalankan, Mockito berada dalam mode perekaman: panggilan yang ditulis di dalam argumen given tidak benar-benar mengeksekusi logika, melainkan didaftarkan sebagai aturan jawaban. Panggilan willReturn melengkapi aturan itu. Gaya given/willReturn identik fungsinya dengan when/thenReturn klasik; gaya BDD hanya membuat urutan kalimatnya terbaca given-when-then.
Matcher Argumen dan Verifikasi Interaksi #
Verifikasi menjadi ringkas berkat matcher bawaan:
verify(pembayaran).bayar(eq("PSN-1"), anyLong()); // jumlah berapa pun
verify(pembayaran, times(1)).bayar(anyString(), anyLong());
verify(pembayaran, never()).bayar(eq("PSN-9"), anyLong());Matcher anyString(), anyLong(), dan eq(...) menggantikan pembandingan eksak; times(1) dan never() mengatur berapa kali panggilan diharapkan. Satu aturan penting: matcher tidak boleh dicampur dengan argumen biasa — begitu satu argumen memakai matcher, argumen lain wajib dibungkus eq(...).
Menangkap Argumen dengan ArgumentCaptor #
Kadang nilai argumen baru diketahui setelah kode berjalan, atau kita ingin memeriksa argumen yang dikonstruksi di dalam method produksi. ArgumentCaptor merekam argumen asli untuk diperiksa belakangan:
@Captor
ArgumentCaptor<Long> jumlahCaptor;
@Test
void jumlahPesananDiteruskanKeGateway() {
kasir.proses(new Pesanan("PSN-1", 250_000L));
verify(pembayaran).bayar(eq("PSN-1"), jumlahCaptor.capture());
assertThat(jumlahCaptor.getValue()).isEqualTo(250_000L);
}Panggilan capture() bertindak sebagai matcher yang menampung argumen aktual ke dalam captor; getValue() mengambil nilai terakhir dan getAllValues() seluruh nilai bila method dipanggil berkali-kali.
Padanan Manual dari Java Murni #
Apa yang sebenarnya dilakukan mock, jawaban, dan verify? Program berikut membangun padanan mini tanpa Mockito: class FakePembayaran merekam setiap panggilan sebagai record, jawaban dapat dipasang ulang antar skenario, dan method pemeriksaan bertindak sebagai verify serta captor:
import java.util.ArrayList;
import java.util.List;
public class ContohVerifikasiInteraksi {
record HasilBayar(boolean sukses, String kode) {}
interface GatewayPembayaran {
HasilBayar bayar(String idPesanan, long jumlah);
}
// ---- padanan mock(): merekam semua panggilan, jawaban dapat dipasang ----
static final class FakePembayaran implements GatewayPembayaran {
record Panggilan(String idPesanan, long jumlah) {}
private final List<Panggilan> rekaman = new ArrayList<>();
private HasilBayar jawabanBerikutnya = new HasilBayar(true, "OK");
void jawabDengan(HasilBayar hasil) {
this.jawabanBerikutnya = hasil;
}
@Override
public HasilBayar bayar(String idPesanan, long jumlah) {
rekaman.add(new Panggilan(idPesanan, jumlah));
return jawabanBerikutnya;
}
// padanan verify(gateway).bayar(id, ...)
boolean pernahMemanggil(String idPesanan) {
return rekaman.stream().anyMatch(p -> p.idPesanan().equals(idPesanan));
}
// padanan ArgumentCaptor: ambil ulang argumen yang diteruskan
long jumlahUntuk(String idPesanan) {
return rekaman.stream()
.filter(p -> p.idPesanan().equals(idPesanan))
.findFirst()
.map(Panggilan::jumlah)
.orElse(-1L);
}
int totalPanggilan() {
return rekaman.size();
}
}
record Pesanan(String id, long total) {}
static final class Kasir {
private final GatewayPembayaran pembayaran;
Kasir(GatewayPembayaran pembayaran) {
this.pembayaran = pembayaran;
}
String proses(Pesanan pesanan) {
HasilBayar hasil = pembayaran.bayar(pesanan.id(), pesanan.total());
if (!hasil.sukses()) {
throw new IllegalStateException("pembayaran ditolak: " + hasil.kode());
}
return "STRUK-" + pesanan.id();
}
}
public static void main(String[] args) {
FakePembayaran mock = new FakePembayaran();
mock.jawabDengan(new HasilBayar(true, "OK-42")); // pasang jawaban
Kasir kasir = new Kasir(mock);
String struk = kasir.proses(new Pesanan("PSN-1", 250_000L));
System.out.println("struk : " + struk);
System.out.println("verify PSN-1 dipanggil : " + mock.pernahMemanggil("PSN-1"));
System.out.println("verify jumlah diteruskan : " + mock.jumlahUntuk("PSN-1"));
System.out.println("verify PSN-9 dipanggil : " + mock.pernahMemanggil("PSN-9"));
System.out.println("total panggilan : " + mock.totalPanggilan());
// skenario kedua: jawaban penolakan dari gateway
mock.jawabDengan(new HasilBayar(false, "KURANG-SALDO"));
try {
kasir.proses(new Pesanan("PSN-2", 500_000L));
} catch (IllegalStateException e) {
System.out.println("penolakan : " + e.getMessage());
}
System.out.println("total panggilan : " + mock.totalPanggilan());
}
}Output yang Dihasilkan:
struk : STRUK-PSN-1
verify PSN-1 dipanggil : true
verify jumlah diteruskan : 250000
verify PSN-9 dipanggil : false
total panggilan : 1
penolakan : pembayaran ditolak: KURANG-SALDO
total panggilan : 2Bedah baris-per-baris untuk bagian intinya:
- Baris
private final List<Panggilan> rekaman = new ArrayList<>();: inilah memori mock — setiap panggilanbayardisimpan lengkap dengan argumennya, persis rekaman internal yang dipelihara proxy Mockito. - Baris
void jawabDengan(HasilBayar hasil): padanangiven(...).willReturn(...)— menetapkan jawaban untuk panggilan berikutnya; test kedua memasang jawaban penolakan tanpa membuat mock baru. - Baris
rekaman.add(new Panggilan(idPesanan, jumlah));: method yang di-mock tidak berisi logika nyata; satu-satunya pekerjaannya adalah mencatat lalu mengembalikan jawaban yang dipasang. - Baris
boolean pernahMemanggil(String idPesanan): padananverify(gateway).bayar(eq(idPesanan), anyLong())— memindai rekaman dan melaporkan apakah interaksi tersebut pernah terjadi. - Baris
long jumlahUntuk(String idPesanan): padananArgumentCaptor— mengambil ulang argumenjumlahyang benar-benar diteruskan kode produksi, sehingga test dapat memastikan nilai yang dikirim ke gateway tepat. - Baris
try { kasir.proses(...); } catch (IllegalStateException e): skenario gagal membuktikan jawaban penolakan menjalar menjadi exception di lapisan service — perilaku yang sama yang diuji lewatassertThrowsbila memakai JUnit.
Padanan manual di atas memang lebih panjang, tetapi ia memperlihatkan bahwa mocking sebenarnya sederhana: rekaman panggilan, jawaban yang dipasang, dan pemindaian rekaman. Chapter berikutnya memperjelas peta lengkapnya — membedakan stub, mock, fake, dan spy sebagai satu keluarga test double.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohVerifikasiInteraksi.java
Unduh berkasimport java.util.ArrayList;
import java.util.List;
public class ContohVerifikasiInteraksi {
record HasilBayar(boolean sukses, String kode) {}
interface GatewayPembayaran {
HasilBayar bayar(String idPesanan, long jumlah);
}
// ---- padanan mock(): merekam semua panggilan, jawaban dapat dipasang ----
static final class FakePembayaran implements GatewayPembayaran {
record Panggilan(String idPesanan, long jumlah) {}
private final List<Panggilan> rekaman = new ArrayList<>();
private HasilBayar jawabanBerikutnya = new HasilBayar(true, "OK");
void jawabDengan(HasilBayar hasil) {
this.jawabanBerikutnya = hasil;
}
@Override
public HasilBayar bayar(String idPesanan, long jumlah) {
rekaman.add(new Panggilan(idPesanan, jumlah));
return jawabanBerikutnya;
}
// padanan verify(gateway).bayar(id, ...)
boolean pernahMemanggil(String idPesanan) {
return rekaman.stream().anyMatch(p -> p.idPesanan().equals(idPesanan));
}
// padanan ArgumentCaptor: ambil ulang argumen yang diteruskan
long jumlahUntuk(String idPesanan) {
return rekaman.stream()
.filter(p -> p.idPesanan().equals(idPesanan))
.findFirst()
.map(Panggilan::jumlah)
.orElse(-1L);
}
int totalPanggilan() {
return rekaman.size();
}
}
record Pesanan(String id, long total) {}
static final class Kasir {
private final GatewayPembayaran pembayaran;
Kasir(GatewayPembayaran pembayaran) {
this.pembayaran = pembayaran;
}
String proses(Pesanan pesanan) {
HasilBayar hasil = pembayaran.bayar(pesanan.id(), pesanan.total());
if (!hasil.sukses()) {
throw new IllegalStateException("pembayaran ditolak: " + hasil.kode());
}
return "STRUK-" + pesanan.id();
}
}
public static void main(String[] args) {
FakePembayaran mock = new FakePembayaran();
mock.jawabDengan(new HasilBayar(true, "OK-42")); // pasang jawaban
Kasir kasir = new Kasir(mock);
String struk = kasir.proses(new Pesanan("PSN-1", 250_000L));
System.out.println("struk : " + struk);
System.out.println("verify PSN-1 dipanggil : " + mock.pernahMemanggil("PSN-1"));
System.out.println("verify jumlah diteruskan : " + mock.jumlahUntuk("PSN-1"));
System.out.println("verify PSN-9 dipanggil : " + mock.pernahMemanggil("PSN-9"));
System.out.println("total panggilan : " + mock.totalPanggilan());
// skenario kedua: jawaban penolakan dari gateway
mock.jawabDengan(new HasilBayar(false, "KURANG-SALDO"));
try {
kasir.proses(new Pesanan("PSN-2", 500_000L));
} catch (IllegalStateException e) {
System.out.println("penolakan : " + e.getMessage());
}
System.out.println("total panggilan : " + mock.totalPanggilan());
}
}