Istilah “mock” sering dipakai orang untuk semua pengganti dependensi di test, padahal keluarganya memiliki anggota lebih dari satu — dan tiap anggota menyelesaikan masalah yang berbeda. Stub mengisi kebutuhan akan jawaban siap pakai, mock mengisi kebutuhan akan bukti interaksi, dan fake mengisi kebutuhan akan implementasi yang benar-benar bekerja dalam versi ringkas. Salah memilih anggota keluarga membuat test lebih rumit dari yang diperlukan atau lebih rapuh dari yang seharusnya. Pada Chapter ini kita akan memetakan ketiganya lewat satu domain yang sama, membandingkan perannya dalam tabel, lalu menjalankan ketiganya dalam satu program Java murni supaya perbedaannya terlihat langsung.
Satu Keluarga Test Double #
Istilah payungnya adalah test double — mengikuti istilah stunt double untuk pemeran pengganti dalam film. Ketika dependensi sungguhan tidak praktik dihadirkan di test, kita pasang pengganti yang lebih ringan. Anggota keluarga yang akan kita fokuskan: stub memberi jawaban tetap untuk dipakai kode, mock merekam interaksi untuk diverifikasi test, dan fake menyediakan implementasi fungsional yang lebih sederhana dari yang sungguhan. Ada pula spy — pembungkus objek nyata yang menambahkan perekaman panggilan — yang kita sebut singkat karena penggunaannya lebih jarang.
Perbedaan ketiganya paling mudah diingat lewat pertanyaan yang mereka jawab:
| Jenis | Menjawab pertanyaan | Contoh wujud | Umur pakai |
|---|---|---|---|
| Stub | “Apa hasil dari cari(id)?” | Hard-code satu nilai balik | Satu test |
| Mock | “Apakah simpan(p) benar dipanggil?” | Daftar rekaman panggilan | Satu test |
| Fake | “Apakah data tersimpan dan bisa diambil lagi?” | Repository ber-HashMap | Banyak test |
Stub untuk Jawaban Siap Pakai #
Ketika kode yang diuji hanya perlu membaca dari dependensi, stub memadai. Misalnya method sudahTerdaftar(id) hanya bertanya cari(id) != null ke repository. Stub StubRepo kita isi satu nilai tetap: new StubRepo(dewi) untuk skenario terdaftar, new StubRepo(null) untuk skenario tidak ada. Stub tidak memeriksa apa pun — ia hanya bicara ketika ditanya. Menggunakan mock di sini akan menulis verifikasi panggilan cari yang tidak menambah keyakinan apa pun terhadap perilaku yang diinginkan.
Mock untuk Bukti Interaksi #
Ketika yang ingin dibuktikan adalah kejadian — pendaftaran valid harus menyimpan data ke repository — jawaban tidak cukup; kita butuh bukti bahwa simpan dipanggil dengan argumen yang benar. Mock menyimpan rekaman panggilan, lalu test memeriksa rekaman itu: apakah dipanggil, berapa kali, dengan argumen apa. Kekuatannya sekaligus batasnya: mock terikat pada detail interaksi, sehingga penyusunan ulang internal (misalnya mulai memakai operasi massal simpanSemua) dapat mematahkan test meski hasil akhir bagi pengguna tidak berubah.
Fake untuk Implementasi yang Bekerja #
Fake adalah implementasi sungguhan dalam bentuk termurah: FakeRepo menyimpan pengguna di HashMap sehingga simpan lalu cari benar-benar bekerja bolak-balik. Karena berperilaku seperti dependensi aslinya, fake dapat dipakai banyak test sekaligus dan bahkan dipakai integration test kecil. Konsekuensinya, fake sendiri kini adalah kode yang harus benar — lebih banyak baris untuk ditulis dan dirawat dibanding stub atau mock. Database in-memory seperti H2 untuk menggantikan PostgreSQL produksi, atau repository ber-HashMap menggantikan tabel, adalah fake dalam arti ini.
Ketiganya dalam Satu Program #
Program berikut menguji LayananPendaftaran yang sama tiga kali dengan tiga pengganti berbeda. Bandingkan bagaimana masing-masing menjawab pertanyaan yang berbeda:
import java.util.HashMap;
import java.util.Map;
public class ContohTestDouble {
record Pengguna(String id, String nama, int umur) {}
interface RepoPengguna {
Pengguna cari(String id);
void simpan(Pengguna pengguna);
}
// ---- kode produksi yang bergantung pada repository ----
static final class LayananPendaftaran {
private final RepoPengguna repo;
LayananPendaftaran(RepoPengguna repo) {
this.repo = repo;
}
boolean sudahTerdaftar(String id) {
return repo.cari(id) != null;
}
String daftar(Pengguna pengguna) {
if (pengguna.umur() < 17) {
throw new IllegalArgumentException("umur minimal 17: " + pengguna.nama());
}
repo.simpan(pengguna);
return "REG-" + pengguna.id();
}
}
// ---- STUB: jawaban siap pakai untuk query, tanpa verifikasi ----
static final class StubRepo implements RepoPengguna {
private final Pengguna jawaban;
StubRepo(Pengguna jawaban) {
this.jawaban = jawaban;
}
@Override
public Pengguna cari(String id) {
return jawaban;
}
@Override
public void simpan(Pengguna pengguna) {
// tidak relevan untuk skenario query
}
}
// ---- MOCK: merekam interaksi agar bisa diverifikasi ----
static final class MockRepo implements RepoPengguna {
private int simpanDipanggil = 0;
private Pengguna argumenTerakhir;
@Override
public Pengguna cari(String id) {
return null;
}
@Override
public void simpan(Pengguna pengguna) {
simpanDipanggil++;
argumenTerakhir = pengguna;
}
boolean simpanPernahDipanggil() {
return simpanDipanggil > 0;
}
Pengguna argumenSimpanTerakhir() {
return argumenTerakhir;
}
}
// ---- FAKE: implementasi ringkas yang benar-benar bekerja ----
static final class FakeRepo implements RepoPengguna {
private final Map<String, Pengguna> penyimpanan = new HashMap<>();
@Override
public Pengguna cari(String id) {
return penyimpanan.get(id);
}
@Override
public void simpan(Pengguna pengguna) {
penyimpanan.put(pengguna.id(), pengguna);
}
int jumlahTersimpan() {
return penyimpanan.size();
}
}
public static void main(String[] args) {
Pengguna dewi = new Pengguna("P-1", "Dewi", 21);
System.out.println("== stub: jawaban siap untuk query ==");
LayananPendaftaran denganStub = new LayananPendaftaran(new StubRepo(dewi));
System.out.println("sudahTerdaftar(P-1) = " + denganStub.sudahTerdaftar("P-1"));
LayananPendaftaran denganStubKosong = new LayananPendaftaran(new StubRepo(null));
System.out.println("sudahTerdaftar(P-2) = " + denganStubKosong.sudahTerdaftar("P-2"));
System.out.println("== mock: verifikasi interaksi ==");
MockRepo mock = new MockRepo();
LayananPendaftaran denganMock = new LayananPendaftaran(mock);
String id = denganMock.daftar(dewi);
System.out.println("id pendaftaran = " + id);
System.out.println("simpan dipanggil = " + mock.simpanPernahDipanggil());
System.out.println("argumen terakhir = " + mock.argumenSimpanTerakhir());
System.out.println("== fake: implementasi ringkas yang bekerja ==");
FakeRepo fake = new FakeRepo();
LayananPendaftaran denganFake = new LayananPendaftaran(fake);
denganFake.daftar(dewi);
denganFake.daftar(new Pengguna("P-2", "Budi", 19));
System.out.println("jumlah tersimpan = " + fake.jumlahTersimpan());
System.out.println("cari(P-2).nama() = " + fake.cari("P-2").nama());
System.out.println("cari(P-9) = " + fake.cari("P-9"));
}
}Output yang Dihasilkan:
== stub: jawaban siap untuk query ==
sudahTerdaftar(P-1) = true
sudahTerdaftar(P-2) = false
== mock: verifikasi interaksi ==
id pendaftaran = REG-P-1
simpan dipanggil = true
argumen terakhir = Pengguna[id=P-1, nama=Dewi, umur=21]
== fake: implementasi ringkas yang bekerja ==
jumlah tersimpan = 2
cari(P-2).nama() = Budi
cari(P-9) = nullTiga bagian keluaran itu berasal dari tiga gaya pengganti. Bedah baris-per-baris untuk bagian-bagian kuncinya:
- Baris
private final Pengguna jawaban;diStubRepo: seluruh isi stub — satu jawaban tetap; methodsimpansengaja kosong karena skenario query tidak membutuhkannya. - Baris
simpanDipanggil++; argumenTerakhir = pengguna;diMockRepo: seluruh isi mock — bukan logika, melainkan perekaman; dua field itulah yang nanti diperiksa test. - Baris
private final Map<String, Pengguna> penyimpanan = new HashMap<>();diFakeRepo: fake menyimpan state nyata;simpanmenulis dancarimembaca dari state yang sama, sehingga roundtrip bekerja. - Baris
denganStub.sudahTerdaftar("P-1"): test berbasis stub memeriksa nilai balik kode produksi; jawaban stub-lah yang menentukan cabang mana kode produksi yang dijalankan. - Baris
mock.simpanPernahDipanggil()danmock.argumenSimpanTerakhir(): test berbasis mock memeriksa rekaman; perhatikan bahwaargumen terakhirtercetak sebagai recordPengguna[id=P-1, ...]—toStringbawaan record memang deterministik. - Baris
fake.cari("P-2").nama()danfake.cari("P-9"): test berbasis fake memeriksa perilaku roundtrip — data yang disimpan bisa diambil kembali, dan id yang tidak ada menghasilkannull.
Memilih Test Double yang Tepat #
Alur keputusannya dapat diringkas. Jika test hanya membaca dari dependensi dan satu nilai cukup, pakai stub. Jika yang ingin dibuktikan adalah terjadi atau tidaknya sebuah aksi — penyimpanan, pengiriman notifikasi, pembatalan — pakai mock (atau verify pada fake). Jika banyak test membutuhkan dependensi yang perilakunya mendekati aslinya, investasikan fake. Naik ke integration test saat kecocokan perilaku dengan sistem nyata menjadi kebutuhan utama, misalnya dialek SQL yang dipakai produksi.
Dengan peta test double lengkap, kita siap melangkah ke lapisan kedua piramida: integration test yang menggabungkan beberapa komponen nyata dan menguji batas antar komponen secara terkendali.
Referensi #
Berkas Sumber
Berkas pendukung halaman ini dapat diunduh dan dijalankan langsung sesuai penjelasan di atas.
ContohTestDouble.java
Unduh berkasimport java.util.HashMap;
import java.util.Map;
public class ContohTestDouble {
record Pengguna(String id, String nama, int umur) {}
interface RepoPengguna {
Pengguna cari(String id);
void simpan(Pengguna pengguna);
}
// ---- kode produksi yang bergantung pada repository ----
static final class LayananPendaftaran {
private final RepoPengguna repo;
LayananPendaftaran(RepoPengguna repo) {
this.repo = repo;
}
boolean sudahTerdaftar(String id) {
return repo.cari(id) != null;
}
String daftar(Pengguna pengguna) {
if (pengguna.umur() < 17) {
throw new IllegalArgumentException("umur minimal 17: " + pengguna.nama());
}
repo.simpan(pengguna);
return "REG-" + pengguna.id();
}
}
// ---- STUB: jawaban siap pakai untuk query, tanpa verifikasi ----
static final class StubRepo implements RepoPengguna {
private final Pengguna jawaban;
StubRepo(Pengguna jawaban) {
this.jawaban = jawaban;
}
@Override
public Pengguna cari(String id) {
return jawaban;
}
@Override
public void simpan(Pengguna pengguna) {
// tidak relevan untuk skenario query
}
}
// ---- MOCK: merekam interaksi agar bisa diverifikasi ----
static final class MockRepo implements RepoPengguna {
private int simpanDipanggil = 0;
private Pengguna argumenTerakhir;
@Override
public Pengguna cari(String id) {
return null;
}
@Override
public void simpan(Pengguna pengguna) {
simpanDipanggil++;
argumenTerakhir = pengguna;
}
boolean simpanPernahDipanggil() {
return simpanDipanggil > 0;
}
Pengguna argumenSimpanTerakhir() {
return argumenTerakhir;
}
}
// ---- FAKE: implementasi ringkas yang benar-benar bekerja ----
static final class FakeRepo implements RepoPengguna {
private final Map<String, Pengguna> penyimpanan = new HashMap<>();
@Override
public Pengguna cari(String id) {
return penyimpanan.get(id);
}
@Override
public void simpan(Pengguna pengguna) {
penyimpanan.put(pengguna.id(), pengguna);
}
int jumlahTersimpan() {
return penyimpanan.size();
}
}
public static void main(String[] args) {
Pengguna dewi = new Pengguna("P-1", "Dewi", 21);
System.out.println("== stub: jawaban siap untuk query ==");
LayananPendaftaran denganStub = new LayananPendaftaran(new StubRepo(dewi));
System.out.println("sudahTerdaftar(P-1) = " + denganStub.sudahTerdaftar("P-1"));
LayananPendaftaran denganStubKosong = new LayananPendaftaran(new StubRepo(null));
System.out.println("sudahTerdaftar(P-2) = " + denganStubKosong.sudahTerdaftar("P-2"));
System.out.println("== mock: verifikasi interaksi ==");
MockRepo mock = new MockRepo();
LayananPendaftaran denganMock = new LayananPendaftaran(mock);
String id = denganMock.daftar(dewi);
System.out.println("id pendaftaran = " + id);
System.out.println("simpan dipanggil = " + mock.simpanPernahDipanggil());
System.out.println("argumen terakhir = " + mock.argumenSimpanTerakhir());
System.out.println("== fake: implementasi ringkas yang bekerja ==");
FakeRepo fake = new FakeRepo();
LayananPendaftaran denganFake = new LayananPendaftaran(fake);
denganFake.daftar(dewi);
denganFake.daftar(new Pengguna("P-2", "Budi", 19));
System.out.println("jumlah tersimpan = " + fake.jumlahTersimpan());
System.out.println("cari(P-2).nama() = " + fake.cari("P-2").nama());
System.out.println("cari(P-9) = " + fake.cari("P-9"));
}
}