Unit testing adalah praktik menulis kode untuk menguji unit kode lain (biasanya satu method atau class) secara terisolasi, memastikan logika tersebut berperilaku sesuai ekspektasi tanpa perlu menjalankan seluruh aplikasi atau bergantung pada database sungguhan.
ASP.NET Core mendukung berbagai framework testing populer seperti xUnit, NUnit, dan MSTest, dengan xUnit menjadi pilihan paling umum di komunitas .NET modern karena sintaksnya yang bersih dan dukungan yang baik dari tooling Visual Studio maupun dotnet CLI.
Artikel ini membahas cara menulis unit test dasar dengan xUnit, memakai mocking untuk mengisolasi dependensi eksternal, serta prinsip Arrange-Act-Assert yang menjadi struktur standar penulisan test yang jelas dan mudah dibaca.
Membuat Project Test dan Test Pertama
Atribut Fact menandai method sebagai satu test case
Arrange-Act-Assert sebagai Struktur Test
AAA memberi struktur yang jelas dan konsisten pada setiap test
Theory untuk Menguji Beberapa Input Sekaligus
Theory menjalankan satu method test dengan beberapa set data berbeda
Mocking Dependensi dengan Moq
Mock menggantikan dependensi asli dengan perilaku yang dikontrol
Mocking Dependency dengan Moq pada Unit Test Service
Unit test OrderService memakai Mock<IProductRepository>, tanpa database nyata
Unit test yang baik untuk business logic di ASP.NET Core seharusnya tidak bergantung pada database, API eksternal, atau file system yang sesungguhnya, karena dependency semacam itu membuat test lambat, rapuh, dan sulit dijalankan berulang kali di pipeline CI/CD. Library Moq memungkinkan pembuatan objek palsu (mock) dari sebuah interface seperti IProductRepository, lengkap dengan kontrol penuh atas nilai apa yang dikembalikan ketika method tertentu dipanggil.
Pola Arrange-Act-Assert pada contoh di atas memisahkan tiga tahap test secara jelas: Arrange menyiapkan mock dan data awal, Act menjalankan method yang diuji, dan Assert memverifikasi hasilnya. Method repoMock.Verify() di akhir memastikan GetByIdAsync benar-benar dipanggil tepat satu kali, berguna untuk memastikan service tidak melakukan query database berulang kali secara tidak perlu untuk operasi yang sama.
Agar sebuah class service benar-benar bisa diuji dengan mock seperti ini, dependency-nya harus didaftarkan lewat interface dan disuntikkan lewat constructor, bukan dibuat langsung di dalam method (misalnya new ProductRepository() di dalam OrderService). Prinsip ini sejalan dengan Dependency Injection yang sudah dibahas sebelumnya, sekaligus menjadi alasan utama mengapa desain yang loosely coupled jauh lebih mudah diuji dibandingkan kode yang dependency-nya ditulis secara hardcoded.
Integration Testing dengan WebApplicationFactory
WebApplicationFactory menjalankan seluruh pipeline aplikasi di dalam proses test
Unit test dengan mocking seperti yang dibahas pada round sebelumnya memverifikasi logika bisnis secara terisolasi, tetapi tidak menjamin bahwa routing, middleware, model binding, dan serialisasi JSON benar-benar bekerja sama dengan baik ketika digabungkan dalam satu aplikasi utuh. WebApplicationFactory
Method WithWebHostBuilder memungkinkan override konfigurasi service khusus untuk kebutuhan test, paling umum dipakai untuk mengganti DbContext yang terhubung ke SQL Server sungguhan dengan UseInMemoryDatabase, sehingga test integrasi tidak bergantung pada ketersediaan database eksternal dan bisa dijalankan berulang kali dengan state yang bersih setiap kali test dimulai. HttpClient yang dihasilkan dari factory ini berperilaku persis seperti client yang memanggil API sungguhan lewat jaringan. Header seperti Authorization pun bisa ditambahkan langsung ke HttpClient ini sebelum memanggil endpoint yang membutuhkan autentikasi.
Integration test jenis ini sangat berguna untuk memverifikasi bahwa kontrak API benar-benar sesuai ekspektasi, termasuk status code, format response, dan header seperti Content-Type, yang tidak bisa diuji lewat unit test murni terhadap service tanpa HTTP pipeline. Karena menjalankan lebih banyak komponen sekaligus, integration test cenderung lebih lambat dibanding unit test, sehingga biasanya ditempatkan pada tahap CI/CD terpisah dan dijalankan lebih jarang dibanding unit test yang dijalankan di setiap commit. Kombinasi unit test untuk logika murni dan integration test untuk kontrak API memberi cakupan pengujian yang lebih menyeluruh dibanding mengandalkan salah satunya saja.
In-Memory Database vs SQLite In-Memory untuk Integration Test EF Core
Provider UseInMemoryDatabase bawaan EF Core memang mudah dipakai, tapi tidak memvalidasi constraint seperti foreign key, unique index, atau concurrency token sebagaimana database relasional sungguhan, sehingga query yang seharusnya gagal dijalankan di SQL Server bisa saja lolos begitu saja saat diuji dengan provider ini. Hal tersebut membuat sebagian bug baru ketahuan setelah aplikasi benar-benar di-deploy ke lingkungan sungguhan.
SQLite in-memory jauh lebih mendekati perilaku database relasional karena benar-benar menjalankan perintah SQL dan menegakkan constraint sebagaimana mestinya, meski tetap ada sedikit perbedaan sintaks dibanding SQL Server untuk fitur-fitur tertentu. Untuk pengujian yang memang menguji query kompleks atau aturan integritas data secara ketat, SQLite in-memory memberikan hasil yang jauh lebih dapat dipercaya dibanding sekadar memakai provider in-memory biasa.
Kesalahan Umum yang Sering Terjadi
Menulis Test yang Bergantung pada Database Sungguhan
Unit test yang terhubung ke database asli menjadi lambat dan rapuh karena bergantung pada kondisi data yang bisa berubah; sebaiknya memakai mock atau in-memory database untuk menjaga test tetap cepat dan terisolasi.
Satu Test Memeriksa Terlalu Banyak Hal Sekaligus
Test yang memiliki banyak Assert untuk berbagai aspek berbeda dalam satu method membuat sulit mengetahui bagian mana yang sebenarnya gagal ketika test tersebut tidak lolos, sebaiknya dipecah menjadi beberapa test yang lebih fokus.
Mengabaikan Test yang Gagal Secara Konsisten
Membiarkan test yang gagal tetap ada tanpa diperbaiki atau dihapus, atau menandainya sebagai Skip tanpa alasan jelas, mengurangi kepercayaan tim terhadap seluruh suite test dan bisa menyembunyikan bug nyata yang sebenarnya perlu diperbaiki.
Kapan Pendekatan Ini Tidak Cocok Dipakai
Unit test membutuhkan waktu tambahan untuk ditulis di awal, namun memberikan jaring pengaman yang sangat berharga ketika kode diubah di kemudian hari, memastikan perubahan tidak merusak fungsionalitas yang sudah ada sebelumnya tanpa disadari.
Mocking memungkinkan test berjalan cepat dan terisolasi dari dependensi eksternal, namun test yang terlalu banyak memakai mock bisa kehilangan kemampuan mendeteksi masalah integrasi nyata antar komponen, sehingga tetap perlu dilengkapi dengan integration test untuk skenario end-to-end yang lebih realistis.
Ringkasan
Unit testing dengan xUnit dan Moq memungkinkan logika aplikasi diuji secara terisolasi dan cepat, dengan pola Arrange-Act-Assert memberikan struktur yang jelas pada setiap test. Investasi waktu menulis unit test yang baik akan terbayar dalam bentuk kepercayaan diri yang lebih tinggi saat melakukan perubahan kode di masa depan.
Seri Belajar .NET Core:
← Sebelumnya: Exception Handling Global Middleware
→ Selanjutnya: Deployment ASP.NET Core ke IIS atau Cloud