Menjalankan unit dan integration test pada pipeline async backend sering kali memunculkan masalah assertion failure yang minim konteks. Makro bawaan standar Rust seperti assert_eq! membandingkan nilai berdasarkan trait PartialEq dan memformat kegagalan menggunakan fmt::Debug. Ketika memvalidasi struct bersarang dengan banyak field, output terminal sering kali menghasilkan ratusan baris data tanpa visualisasi field mana yang sebenarnya tidak cocok, atau memotong state internal penting karena evaluasi boolean.

Masalah: Kegagalan Assertion pada Async Backend Test

Pertimbangkan kasus verifikasi endpoint order processing yang mengembalikan state transaksi backend. Handler menjalankan alur asynchronous, memperbarui database, dan mengembalikan model agregat seperti berikut:

#[derive(Debug, PartialEq)]
pub struct OrderResponse {
    pub order_id: String,
    pub user_id: u64,
    pub total_amount: u64,
    pub status: OrderStatus,
    pub items: Vec<OrderItem>,
    pub metadata: HashMap<String, String>,
}

#[derive(Debug, PartialEq)]
pub enum OrderStatus {
    Pending,
    Settled,
    Failed(String),
}

#[derive(Debug, PartialEq)]
pub struct OrderItem {
    pub sku: String,
    pub qty: u32,
    pub price: u64,
}

Pengujian standar backend umumnya ditulis dengan mengevaluasi kondisi menggunakan boolean assertion atau assert_eq!:

#[tokio::test]
async fn test_process_order_failed() {
    let client = TestApiClient::new().await;
    let response = client.process_order("ORD-101", 42).await;

    // Masalah: Menggunakan ekspresi boolean menghilangkan nilai aktual vs ekspresi
    assert!(response.status == OrderStatus::Settled && response.items.len() == 2);
}

Jika verifikasi di atas gagal, test runner hanya mencetak pesan panic tanpa konteks state:

thread 'test_process_order_failed' panicked at src/tests.rs:18:5:
assertion failed: response.status == OrderStatus::Settled && response.items.len() == 2
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace

Output ini tidak menjelaskan status aktual pesanan, jumlah item yang diterima, maupun data yang menyimpang.

Root Cause: Keterbatasan Makro Bawaan

Penggunaan assert_eq! memberikan perbaikan minor namun tetap bermasalah untuk tipe data berukuran besar:

#[tokio::test]
async fn test_process_order_verbose() {
    let client = TestApiClient::new().await;
    let response = client.process_order("ORD-101", 42).await;
    let expected = OrderResponse { /* struct lengkap dengan 15 field */ };

    assert_eq!(response, expected);
}

Dua keterbatasan utama dari pendekatan ini adalah:

  • Opaque Failures: Operasi logika (&&, ||) dievaluasi menjadi nilai tunggal bool sebelum dievaluasi oleh makro assert!, sehingga informasi sub-ekspresi hilang.
  • Noise Formatting: Makro assert_eq! mencetak seluruh implementasi Debug dari kedua sisi. Apabila field metadata atau items berisi data panjang, developer harus memeriksa teks puluhan baris secara manual untuk menemukan deviasi 1 baris.

Solusi: Assertion Deklaratif dengan test-that

Pola framework testing assertion deklaratif ala pustaka test-that menyelesaikan masalah ini dengan mengurai pohon sintaks assertion (syntax tree inspection) dan menyediakan matcher modular yang mengevaluasi properti spesifik tanpa membuang context tree.

Tambahkan dependensi ke dalam Cargo.toml:

[dev-dependencies]
test-that = "0.1"

Tulis ulang assertion pengujian menggunakan makro test_that! dan matcher deklaratif:

use test_that::test_that;
use test_that::matchers::{be_equal_to, has_len, field};

#[tokio::test]
async fn test_process_order_with_test_that() {
    let client = TestApiClient::new().await;
    let response = client.process_order("ORD-101", 42).await;

    test_that!(response, field!(status, be_equal_to(OrderStatus::Settled)));
    test_that!(response.items, has_len(2));
}

Analisis Output Diff

Ketika assertion gagal (misalnya sistem mengembalikan OrderStatus::Failed("insufficient_funds")), output pengujian langsung mengisolasi path struct yang bermasalah:

assertion failed: `test_that!(response, field!(status, be_equal_to(OrderStatus::Settled)))`
  Value: OrderResponse { order_id: "ORD-101", user_id: 42, total_amount: 150000, ... }
  Field `.status` failed matcher:
    Expected: OrderStatus::Settled
    Actual:   OrderStatus::Failed("insufficient_funds")
  Diff:
    OrderStatus::Settled
  - OrderStatus::Failed("insufficient_funds")

Konteks kegagalan diekstraksi ke level field. Root cause kesalahan logika backend langsung terlihat tanpa memerlukan logging tambahan atau sesi debugging via gdb/lldb.

Verifikasi Regresi

Untuk memastikan kestabilan test suite pada continuous integration (CI), validasi seluruh jalur error menggunakan matcher yang menangkap expected failure secara eksplisit:

#[tokio::test]
async fn test_regression_insufficient_funds() {
    let client = TestApiClient::new().await;
    let response = client.process_order("ORD-ERR-01", 42).await;

    test_that!(response.status, be_equal_to(OrderStatus::Failed("insufficient_funds".into())));
}

Jalankan test suite menggunakan perintah Cargo standar:

cargo test --test integration_tests -- test_regression_insufficient_funds --nocapture

Eksekusi berhasil tanpa panic, memvalidasi bahwa penanganan kegagalan bisnis berjalan sesuai spesifikasi kontrak API.