Kegagalan pengujian integrasi yang tidak konsisten (flaky test) pada aplikasi Actix Web sering kali bermuara pada benturan soket jaringan OS. Ketika pengujian mencoba menginisialisasi server HTTP live dengan binding alamat TCP statis, eksekusi paralel Rust secara bawaan memicu tabrakan port.
Akar Masalah: EADDRINUSE pada Eksekusi Paralel
Secara default, cargo test mengeksekusi fungsi tes dalam thread terpisah secara paralel. Pendekatan pengujian yang memanggil HttpServer::bind("127.0.0.1:8080") di setiap unit tes integrasi akan memicu race condition pada alokasi port sistem operasi.
Ketika dua thread mencoba melakukan binding ke alamat IP dan port yang identik sebelum thread pertama melepaskan listener-nya, OS mengembalikan error std::io::ErrorKind::AddrInUse (POSIX: EADDRINUSE). Menjalankan tes secara serial menggunakan cargo test -- --test-threads=1 memang mengeliminasi tabrakan tersebut, namun secara drastis memperlambat eksekusi pipeline CI Anda.
Solusi In-Memory: actix_web::test::init_service
Pendekatan yang benar untuk integration test level API adalah menghindari alokasi TCP socket OS sama sekali. Modul actix_web::test menyediakan fungsi init_service, yang menginisialisasi router dan middleware pipeline Actix Web langsung di memori sebagai implementasi trait Service.
Dengan init_service, alur data diproses secara internal:
TestRequest: Membuat structHttpRequestsintetis tanpa melalui network stack.call_service: Menyerahkan request langsung ke router Actix untuk dieksekusi oleh pipeline extractor, middleware, dan handler.- Zero Network Overhead: Tidak ada port yang dibuka, tidak ada handshake TCP, dan aman dieksekusi ratusan thread sekaligus tanpa risiko port collision.
Implementasi Praktis: Setup App, Dependency Injection, dan Asersi
Pola arsitektur terbaik mengharuskan pemisahan konfigurasi routing dari instansiasi TCP server fisik. Ekstrak rute ke dalam fungsi konfigurasi independen yang menerima &mut web::ServiceConfig.
Berikut implementasi lengkap pengujian endpoint dengan state injection dan pembacaan respons:
use actix_web::{test, web, App, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use std::sync::Mutex;
#[derive(Serialize, Deserialize, Clone)]
pub struct Item {
pub id: u32,
pub name: String,
}
pub struct AppState {
pub items: Mutex<Vec<Item>>,
}
async fn create_item(item: web::Json<Item>, data: web::Data<AppState>) -> impl Responder {
let mut items = data.items.lock().unwrap();
items.push(item.clone());
HttpResponse::Created().json(item.into_inner())
}
// Pisahkan konfigurasi rute agar dapat digunakan ulang oleh main.rs dan modul test
pub fn configure_routes(cfg: &mut web::ServiceConfig) {
cfg.service(web::resource("/items").route(web::post().to(create_item)));
}
#[actix_web::test]
async fn test_create_item_success() {
// 1. Setup Dependency State
let app_state = web::Data::new(AppState {
items: Mutex::new(Vec::new()),
});
// 2. Inisialisasi Service In-Memory via init_service
let app = test::init_service(
App::new()
.app_data(app_state.clone())
.configure(configure_routes),
)
.await;
// 3. Bangun Request Simulasi via TestRequest
let payload = Item {
id: 101,
name: String::from("Rust Book"),
};
let req = test::TestRequest::post()
.uri("/items")
.set_json(&payload)
.to_request();
// 4. Eksekusi Request via call_service
let resp = test::call_service(&app, req).await;
assert_eq!(resp.status(), 201);
// 5. Asersi Response Body
let body: Item = test::read_body_json(resp).await;
assert_eq!(body.id, 101);
assert_eq!(body.name, "Rust Book");
// Verifikasi mutasi state
let state_items = app_state.items.lock().unwrap();
assert_eq!(state_items.len(), 1);
}
Perbandingan: init_service vs test::TestServer (Ephemeral Port)
Memilih antara mock in-memory dan live server dinamis bergantung pada cakupan pengujian yang ingin dicapai:
| Karakteristik | actix_web::test::init_service | actix_web::test::TestServer |
|---|---|---|
| Transport Layer | In-memory (Tanpa soket OS) | TCP loopback (OS Ephemeral Port) |
| Kecepatan Eksekusi | Sangat tinggi | Sedang (overhead syscall & socket handshake) |
| Port Collision | Mustahil terjadi | Dicegah OS (port 0), tapi rentan resource exhaustion |
| Pengujian Client HTTP | Hanya via TestRequest bawaan |
Bebas memakai client eksternal (Reqwest, cURL) |
| Cakupan Testing | Handler, Middleware, Guard, Extractor | Full stack: TLS handshake, connection timeout, HTTP/2 framing |
Gunakan test::TestServer jika pengujian mengharuskan verifikasi perilaku network level seperti Keep-Alive timeout, streaming payload berukuran gigabyte, atau kompatibilitas TLS. Untuk 90% kasus uji integrasi logika bisnis API, init_service adalah pilihan terbaik.
Praktik Terbaik Arsitektur Test untuk Mencegah Regresi CI
Terapkan pola-pola berikut pada repositori proyek untuk menjamin pipeline CI stabil:
- Hindari Hardcoded Host & Port: Jangan pernah menuliskan
127.0.0.1:8080atau konstanta port serupa di dalam direktoritests/. - Gunakan Pattern Test Factory: Buat helper function seperti
spawn_app()yang bertugas memaketkan inisialisasi state, mock database pool, daninit_serviceagar setup tes tetap ringkas (DRY). - Isolasi Database State: Jika tes in-memory terhubung ke database asli via connection pool, gunakan schema acak per thread atau bungkus setiap transaksi tes dalam database rollback.
Komentar
0 komentar
Masuk ke akun kamu untuk ikut berkomentar.
Belum ada komentar
Jadilah yang pertama ikut berdiskusi!