Kelas yang diuji hampir selalu bergantung pada komponen lain, misalnya penyimpanan data, layanan jaringan, atau jam sistem, dan pengujian yang menemui komponen tersebut secara nyata menjadi lambat serta hasilnya tidak pasti. Mocking mengatasi hal ini dengan menggantikan dependensi berupa objek tiruan yang perilakunya dikendalikan sepenuhnya oleh pengujian. Topik ini membahas interface sebagai titik sambung substitusi, objek tiruan yang ditulis manual, lalu dua pustaka mocking yang populer di .NET, yaitu Moq dan NSubstitute.
Interface sebagai titik sambung#
Substitusi dependensi hanya mungkin apabila kelas yang diuji bergantung pada abstraksi, bukan pada implementasi konkret. Interface menyediakan abstraksi tersebut, sehingga kelas yang menerimanya melalui konstruktor dapat menerima implementasi nyata pada aplikasi dan implementasi tiruan pada pengujian. Pola ini juga membuat batas tanggung jawab antar kelas menjadi eksplisit karena seluruh interaksi melewati kontrak yang ditulis di interface.
Berkas berikut memuat kontrak IPenyimpanCatatan beserta LayananCatatan sebagai kelas yang diuji. Metode TambahCatatan memeriksa judul terlebih dahulu, lalu menyimpan catatan melalui dependensinya, sedangkan AmbilCatatan meneruskan pencarian dan mengembalikan hasilnya apa adanya.
// Interface penyimpanan menjadi titik sambung bagi objek tiruan.
public interface IPenyimpanCatatan
{
void Simpan(Catatan catatan);
Catatan? Cari(string judul);
}
public record Catatan(string Judul, string Isi);
// Kelas yang diuji bergantung pada interface, bukan pada implementasi nyata.
public class LayananCatatan
{
private readonly IPenyimpanCatatan _penyimpanan;
public LayananCatatan(IPenyimpanCatatan penyimpanan)
{
_penyimpanan = penyimpanan;
}
public void TambahCatatan(string judul, string isi)
{
if (string.IsNullOrWhiteSpace(judul))
{
throw new ArgumentException("Judul catatan tidak boleh kosong.");
}
_penyimpanan.Simpan(new Catatan(judul, isi));
}
public Catatan? AmbilCatatan(string judul)
{
return _penyimpanan.Cari(judul);
}
}Fake yang ditulis manual#
Objek tiruan paling sederhana adalah fake, yaitu kelas yang mengimplementasikan interface dengan cara sesederhana mungkin, misalnya menyimpan data ke dalam daftar di memori. Fake tidak memerlukan pustaka apa pun dan perilakunya mudah dibaca. Pengujian berikut memverifikasi bahwa TambahCatatan benar-benar menyimpan satu catatan dengan memeriksa isi daftar milik fake tersebut.
using Xunit;
// Fake yang ditulis manual: implementasi sederhana dari IPenyimpanCatatan.
public class LayananCatatanFakeTests
{
private class PenyimpananPalsu : IPenyimpanCatatan
{
public List<Catatan> Tersimpan { get; } = new();
public void Simpan(Catatan catatan)
{
Tersimpan.Add(catatan);
}
public Catatan? Cari(string judul)
{
return Tersimpan.FirstOrDefault(c => c.Judul == judul);
}
}
[Fact]
public void TambahCatatan_DenganFake_MenyimpanSatuCatatan()
{
var penyimpanan = new PenyimpananPalsu();
var layanan = new LayananCatatan(penyimpanan);
layanan.TambahCatatan("Belanja", "Gula dan teh");
Assert.Single(penyimpanan.Tersimpan);
}
}Fake manual cepat terasa kurang praktis ketika interface bertambah besar. Setiap interface memerlukan kelas baru, skenario gagal seperti penyimpanan yang melempar exception perlu ditulis sebagai variasi kelas tambahan, dan pemeriksaan berupa pemanggilan yang tidak boleh terjadi tidak memiliki tempat yang alami. Pustaka mocking menyelesaikan ketiganya dengan membuat implementasi tiruan secara dinamis.
Mocking dengan Moq#
Moq membuat implementasi interface saat pengujian berjalan melalui kelas Mock<T>, dan properti Object-nya dipakai sebagai dependensi. Konfigurasi perilaku ditulis dengan Setup yang mengembalikan nilai melalui Returns, sedangkan pemeriksaan interaksi ditulis dengan Verify yang membandingkan jumlah pemanggilan melalui Times. Pencocokan argumen menggunakan It.IsAny untuk nilai apa pun dan It.Is untuk kondisi tertentu.
using Moq;
using Xunit;
// Moq membuat implementasi IPenyimpanCatatan secara dinamis.
public class LayananCatatanMoqTests
{
[Fact]
public void TambahCatatan_MenyimpanCatatanKePenyimpanan()
{
var mock = new Mock<IPenyimpanCatatan>();
var layanan = new LayananCatatan(mock.Object);
layanan.TambahCatatan("Belanja", "Gula dan teh");
mock.Verify(p => p.Simpan(It.Is<Catatan>(c => c.Judul == "Belanja")), Times.Once);
}
[Fact]
public void TambahCatatan_JudulKosong_PenyimpananTidakDipanggil()
{
var mock = new Mock<IPenyimpanCatatan>();
var layanan = new LayananCatatan(mock.Object);
Assert.Throws<ArgumentException>(() => layanan.TambahCatatan("", "isi"));
mock.Verify(p => p.Simpan(It.IsAny<Catatan>()), Times.Never);
}
[Fact]
public void AmbilCatatan_MengembalikanNilaiDariSetup()
{
var mock = new Mock<IPenyimpanCatatan>();
mock.Setup(p => p.Cari("Belanja"))
.Returns(new Catatan("Belanja", "Gula dan teh"));
var layanan = new LayananCatatan(mock.Object);
Catatan? hasil = layanan.AmbilCatatan("Belanja");
Assert.NotNull(hasil);
Assert.Equal("Gula dan teh", hasil!.Isi);
}
}Pengujian kedua menunjukkan kekuatan Times.Never yang tidak dimiliki fake manual, yaitu memastikan penyimpanan sama sekali tidak dipanggil apabila judul kosong. Proyek uji berikut memasang package Moq dan NSubstitute sekaligus, lalu menjalankan pengujian fake dan Moq dengan filter nama kelasnya.
# membuat proyek uji xUnit di dalam folder kerja
dotnet new xunit -o UjiMock 2>&1 | grep "Restore succeeded"Restore succeeded.# memasang package Moq dan NSubstitute, keluaran restorasinya disembunyikan
dotnet add UjiMock package Moq > /dev/null 2>&1 && echo "Moq terpasang"
dotnet add UjiMock package NSubstitute > /dev/null 2>&1 && echo "NSubstitute terpasang"
# menyalin kelas yang diuji dan berkas pengujian, contoh templat dihapus
cp LayananCatatan.cs UjiFake.cs UjiMocking.cs UjiNSubstitute.cs UjiMock/
rm UjiMock/UnitTest1.csMoq terpasang
NSubstitute terpasang# menjalankan pengujian fake dan Moq, hanya baris ringkasan yang ditampilkan
dotnet test UjiMock --filter "FullyQualifiedName~FakeTests|FullyQualifiedName~MoqTests" 2>&1 | grep -E "Passed!|Failed!" | sed 's/, Duration.*//'Passed! - Failed: 0, Passed: 4, Skipped: 0, Total: 4Padanan dengan NSubstitute#
NSubstitute menyediakan kemampuan yang sama dengan gaya penulisan yang lebih ringkas, yaitu tiruan dibuat melalui Substitute.For<T> dan langsung dipakai sebagai objek biasa tanpa pembungkus Mock. Nilai kembalian dikonfigurasi dengan memanggil metodenya lalu menautkan Returns, sedangkan pemeriksaan interaksi ditulis dengan Received beserta jumlah pemanggilan yang diharapkan. Pencocokan argumennya bernama Arg.Is dan Arg.Any.
using NSubstitute;
using Xunit;
// Padanan pengujian Moq dengan sintaks NSubstitute.
public class LayananCatatanNSubstituteTests
{
[Fact]
public void TambahCatatan_PenyimpananMenerimaSatuCatatan()
{
var penyimpanan = Substitute.For<IPenyimpanCatatan>();
var layanan = new LayananCatatan(penyimpanan);
layanan.TambahCatatan("Belanja", "Gula dan teh");
penyimpanan.Received(1).Simpan(Arg.Is<Catatan>(c => c.Judul == "Belanja"));
}
[Fact]
public void AmbilCatatan_MengembalikanNilaiYangDikonfigurasi()
{
var penyimpanan = Substitute.For<IPenyimpanCatatan>();
penyimpanan.Cari("Belanja")
.Returns(new Catatan("Belanja", "Gula dan teh"));
var layanan = new LayananCatatan(penyimpanan);
Catatan? hasil = layanan.AmbilCatatan("Belanja");
Assert.Equal("Gula dan teh", hasil!.Isi);
}
}# menjalankan pengujian NSubstitute, hanya baris ringkasan yang ditampilkan
dotnet test UjiMock --filter "FullyQualifiedName~NSubstituteTests" 2>&1 | grep -E "Passed!|Failed!" | sed 's/, Duration.*//'Passed! - Failed: 0, Passed: 2, Skipped: 0, Total: 2# menghapus proyek uji sementara setelah latihan selesai
rm -rf UjiMockKedua pustaka setara untuk kebutuhan dasar seperti pada contoh, sehingga pilihannya lebih bersifat selera tim daripada kemampuan. Yang penting adalah memilih satu pustaka untuk seluruh jalur proyek agar gaya konfigurasi dan verifikasinya konsisten.
Kesalahan umum#
Rujukan package yang belum dipasang menghasilkan error CS0246 berupa pesan The type or namespace name 'Mock' could not be found saat kompilasi. Penyebabnya adalah berkas pengujian memakai using Moq; atau using NSubstitute; padahal dotnet add package-nya belum dijalankan pada proyek uji tersebut, dan perbaikannya adalah memasang package terlebih dahulu lalu membangun ulang.
Kesalahan desain yang paling sering menghambat adalah kelas yang diuji membuat dependensinya sendiri dengan new di dalam metodenya. Kelas semacam ini tidak dapat disubstitusi karena tidak ada celah untuk menyuntikkan tiruan, sehingga pengujian selalu menemui implementasi nyatanya. Perbaikannya adalah mengubah dependensi menjadi parameter konstruktor bertipe interface sebagaimana LayananCatatan pada topik ini.
Perbedaan hasil antar tiruan juga menjadi sumber kebingungan ketika tiruannya dikonfigurasi ulang di tengah pengujian. Konfigurasi Setup pada Moq berlaku dari saat ditetapkan sampai objek tiruannya tidak lagi dipakai, sehingga penimpaan konfigurasi untuk panggilan yang sama perlu disengaja dan ditandai dengan jelas. Pola yang lebih aman adalah membuat tiruan baru untuk setiap kasus pengujian, sehingga konfigurasinya berdiri sendiri dan tidak ikut terbawa dari kasus lain.
Terakhir, jumlah tiruan perlu dijaga tetap kecil. Pengujian yang menyiapkan lima tiruan untuk satu kasus biasanya menandakan kelas yang diuji melakukan terlalu banyak hal sekaligus, dan komposisinya perlu dipecah. Semakin sedikit dependensi yang ditiru, semakin jelas perilaku yang benar-benar diuji oleh kasus tersebut.
Ringkasan#
- Kelas yang bergantung pada interface melalui injeksi konstruktor dapat menerima implementasi tiruan tanpa perubahan kode.
- Fake manual adalah implementasi sederhana yang ditulis sendiri dan cukup untuk interface kecil, tetapi tidak menyediakan verifikasi interaksi.
- Moq mengonfigurasi tiruan melalui
SetupdanVerifypada kelasMock<T>, sedangkan NSubstitute memakaiSubstitute.For<T>denganReturnsdanReceived. Times.NeverdanReceived(0)memeriksa pemanggilan yang tidak boleh terjadi, sesuatu yang tidak alami pada fake manual.
Referensi#
- Moq — devlooped — Repositori resmi Moq beserta dokumentasi Setup, Verify, dan pencocokan argumen.
- NSubstitute — NSubstitute project — Situs resmi NSubstitute dengan panduan Received, Returns, dan Arg.