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 struct HttpRequest sintetis 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:8080 atau konstanta port serupa di dalam direktori tests/.
  • Gunakan Pattern Test Factory: Buat helper function seperti spawn_app() yang bertugas memaketkan inisialisasi state, mock database pool, dan init_service agar 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.