Mocking #
Mocking dalam konteks pengembangan perangkat lunak adalah praktik membuat objek tiruan (mock objects) yang mensimulasikan perilaku dari objek asli yang kompleks, seperti dependensi eksternal, modul jaringan, atau database. Ini berguna dalam pengujian unit (unit testing) untuk memastikan bahwa bagian tertentu dari kode bekerja dengan benar tanpa harus bergantung pada seluruh sistem.
Rust, sebagai bahasa yang fokus pada kinerja dan keamanan memori, memiliki pendekatan khusus dalam hal mocking, terutama karena Rust tidak mendukung mekanisme seperti inheritance dan dynamic dispatch secara langsung seperti di beberapa bahasa OOP lainnya. Mocking dalam Rust biasanya dilakukan menggunakan traits dan beberapa crate atau pustaka pihak ketiga yang menyediakan alat untuk mocking.
Pendekatan Dasar untuk Mocking di Rust #
Karena Rust adalah bahasa yang kuat dalam tipe dan tidak memiliki fitur bawaan untuk mocking seperti beberapa bahasa OOP, mocking biasanya dilakukan dengan memanfaatkan traits. Traits memungkinkan Anda mendefinisikan perilaku yang kemudian bisa diimplementasikan oleh mock structs dalam pengujian.
Menggunakan Traits untuk Mocking #
Langkah pertama dalam mocking di Rust adalah mendefinisikan sebuah trait yang mendeklarasikan perilaku atau metode yang diperlukan oleh kode Anda. Kemudian, Anda dapat mengimplementasikan trait tersebut dalam objek asli dan mock.
Contoh Penggunaan Traits untuk Mocking:
trait DataSource {
fn fetch_data(&self) -> String;
}
struct RealDataSource;
impl DataSource for RealDataSource {
fn fetch_data(&self) -> String {
// Misalnya, melakukan panggilan jaringan atau membaca dari database
"Data asli dari sumber data".to_string()
}
}
struct MockDataSource;
impl DataSource for MockDataSource {
fn fetch_data(&self) -> String {
// Mengembalikan data tiruan untuk pengujian
"Data mock".to_string()
}
}
fn process_data(source: &impl DataSource) {
let data = source.fetch_data();
println!("Memproses: {}", data);
}
fn main() {
let real_source = RealDataSource;
let mock_source = MockDataSource;
process_data(&real_source); // Memanggil dengan sumber data asli
process_data(&mock_source); // Memanggil dengan mock
}
Penjelasan:
DataSource
adalah trait yang mendefinisikan metodefetch_data
.RealDataSource
mengimplementasikanDataSource
untuk menghasilkan data nyata.MockDataSource
mengimplementasikanDataSource
untuk menghasilkan data tiruan yang dapat digunakan dalam pengujian.- Fungsi
process_data
menerima referensi ke tipe apa pun yang mengimplementasikanDataSource
, memungkinkan penggunaan baikRealDataSource
maupunMockDataSource
.
Menggunakan Crate untuk Mocking #
Meskipun pendekatan manual menggunakan traits sudah cukup kuat, ada beberapa crate yang mempermudah dan memperkaya kemampuan mocking di Rust. Beberapa crate populer untuk mocking di Rust termasuk mockall
, mockito
, dan double
.
Mocking dengan mockall
#
mockall
adalah crate yang menyediakan fasilitas lengkap untuk mocking di Rust, termasuk untuk memock trait, fungsi bebas, dan metode statis. mockall
mendukung mocking pada level compile-time dan runtime.
Instalasi mockall
:
Tambahkan crate mockall
ke dalam Cargo.toml
:
[dependencies]
mockall = "0.10.2"
Contoh Penggunaan mockall
:
use mockall::predicate::*;
use mockall::*;
#[automock]
trait DataSource {
fn fetch_data(&self) -> String;
}
fn process_data(source: &impl DataSource) -> String {
source.fetch_data()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_process_data_with_mock() {
let mut mock_source = MockDataSource::new();
mock_source.expect_fetch_data()
.returning(|| "Data mock".to_string());
let result = process_data(&mock_source);
assert_eq!(result, "Data mock");
}
}
Penjelasan:
#[automock]
digunakan untuk secara otomatis menghasilkan mock untuk traitDataSource
.MockDataSource
adalah versi mock dariDataSource
yang dihasilkan olehmockall
.expect_fetch_data
digunakan untuk menentukan perilaku mock ketika metodefetch_data
dipanggil.returning
menentukan nilai yang dikembalikan oleh mock saat metode tersebut dipanggil.
Mocking dengan mockito
#
mockito
adalah crate yang memungkinkan Anda untuk memock server HTTP untuk pengujian. Ini berguna untuk menguji kode yang berinteraksi dengan API eksternal tanpa memerlukan koneksi jaringan nyata.
Instalasi mockito
:
Tambahkan crate mockito
ke dalam Cargo.toml
:
[dependencies]
mockito = "0.31"
Contoh Penggunaan mockito
:
use mockito::{mock, Matcher};
use reqwest;
fn fetch_data_from_api() -> Result<String, reqwest::Error> {
let url = &format!("{}/data", mockito::server_url());
let resp = reqwest::blocking::get(url)?.text()?;
Ok(resp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_fetch_data_from_api() {
let _mock = mock("GET", "/data")
.with_status(200)
.with_body("mocked response")
.create();
let result = fetch_data_from_api().unwrap();
assert_eq!(result, "mocked response");
}
}
Penjelasan:
mockito::mock
digunakan untuk membuat mock server HTTP yang merespons permintaan GET ke/data
dengan status200
dan body"mocked response"
.- Fungsi
fetch_data_from_api
berinteraksi dengan server mock yang disiapkan olehmockito
.
Teknik Lain untuk Mocking #
Dependency Injection #
Dependency Injection (DI) adalah teknik umum dalam pengembangan perangkat lunak yang memungkinkan Anda menyuntikkan dependensi ke dalam objek atau fungsi alih-alih membuatnya secara langsung. Ini sangat mendukung testing dan mocking.
Contoh Dependency Injection:
struct Service<T: DataSource> {
source: T,
}
impl<T: DataSource> Service<T> {
fn new(source: T) -> Self {
Self { source }
}
fn process_data(&self) -> String {
self.source.fetch_data()
}
}
#[cfg(test)]
mod tests {
use super::*;
struct MockDataSource;
impl DataSource for MockDataSource {
fn fetch_data(&self) -> String {
"Mocked data".to_string()
}
}
#[test]
fn test_service_with_mock() {
let mock_source = MockDataSource;
let service = Service::new(mock_source);
assert_eq!(service.process_data(), "Mocked data");
}
}
Penjelasan:
Service<T>
adalah struct generik yang menerima objekT
yang mengimplementasikan traitDataSource
.Service::new
menerima objekT
yang disuntikkan dari luar, memungkinkan pengujian dengan mock.
Mocking dalam Konteks Asynchronous #
Rust mendukung asynchronous programming melalui async
/await
, dan mocking dalam konteks ini memerlukan perhatian khusus.
Contoh Mocking Asynchronous dengan mockall
:
use mockall::predicate::*;
use mockall::*;
#[automock]
#[async_trait::async_trait]
trait AsyncDataSource {
async fn fetch_data(&self) -> String;
}
async fn process_data(source: &impl AsyncDataSource) -> String {
source.fetch_data().await
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn test_process_data_with_mock() {
let mut mock_source = MockAsyncDataSource::new();
mock_source.expect_fetch_data()
.returning(|| Box::pin(async { "Data mock".to_string() }));
let result = process_data(&mock_source).await;
assert_eq!(result, "Data mock");
}
}
Penjelasan:
#[async_trait::async_trait]
memungkinkan Anda untuk mendefinisikan trait asynchronous.Box::pin(async { ... })
digunakan untuk membuatFuture
tiruan yang dapat di-return oleh mock.
Kesimpulan #
Mocking di Rust adalah teknik yang berguna untuk mengisolasi unit kode dalam pengujian, sehingga Anda dapat memastikan bahwa bagian tertentu dari kode Anda bekerja dengan benar tanpa memerlukan dependensi eksternal yang kompleks. Rust tidak memiliki fitur mocking bawaan seperti beberapa bahasa OOP lainnya, tetapi dengan memanfaatkan traits dan crate seperti mockall
, mockito
, dan lainnya, Anda dapat dengan mudah melakukan mocking di Rust.
Memahami dan menggunakan mocking dengan benar akan membantu Anda menulis tes unit yang lebih kuat dan terisolasi, serta memungkinkan Anda untuk membangun aplikasi yang lebih dapat diandalkan.