Unit test yang kita tulis sejauh ini hidup di dalam crate, menempel pada kode produksi, dan bebas menguji fungsi privat sekalipun. Posisi yang nyaman untuk pengembang — namun pengguna nyata crate kita tidak pernah berada di posisi itu. Pengguna hanya melihat apa yang kita beri pub: mereka use nama crate kita, memanggil fungsi publik, dan berhenti di situ. Integration test menempatkan diri persis di posisi pengguna: kode test berada di luar crate, tidak bisa melihat detail internal, dan mengonsumsi antarmuka publik sebagaimana program lain melakukannya. Rust menyiapkan tempat khusus untuk posisi ini — direktori tests di akar proyek — dan cargo test menjalankan isinya sebagai test suite terpisah setelah unit test selesai. Chapter ini membangun crate toko dengan unit test dan integration test berdampingan, membaca laporan dua suite tersebut, lalu membuktikan batas visibilitasnya dengan galat kompilator yang sebenarnya.

Mekanisme: Direktori tests dan Crate Terpisah #

Ketika cargo test berjalan, cargo memeriksa isi direktori tests/ di akar proyek. Setiap file .rs di dalamnya diperlakukan sebagai crate tersendiri yang dikompilasi terpisah dan ditautkan ke pustaka kita — persis seperti crate eksternal mana pun di ekosistem Rust. Konsekuensi mekanisme ini menentukan seluruh karakter integration test: kode di tests/ wajib mengimpor item kita lewat use nama_crate::item, hanya item pub yang terlihat, dan kode bertanda #[cfg(test)] di dalam crate tidak pernah ikut — modul itu memang tidak dikompilasi untuk konsumen luar.

Karena berada di puncak file tanpa modul pembungkus, test integration dilaporkan dengan nama polosnya saja — tanpa prefiks tests:: seperti pada unit test. Setiap file di tests/ juga membentuk binary test tersendiri, sehingga laporan cargo test menampilkan blok running N tests yang terpisah untuk tiap file.

Menyiapkan Crate Toko dengan Dua Suite #

Buat proyek dengan cargo new toko --lib, lalu isi src/lib.rs dengan dua fungsi publik dan satu unit test:

rust
pub fn hitung_diskon(harga: u64, persen: u64) -> u64 {
    harga * persen / 100
}

pub fn harga_setelah_diskon(harga: u64, persen: u64) -> u64 {
    harga - hitung_diskon(harga, persen)
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn hitung_diskon_dua_puluh_persen() {
        assert_eq!(hitung_diskon(200_000, 20), 40_000);
    }
}

Selanjutnya buat file tests/integration_test.rs — cargo mengenali direktori tests secara otomatis tanpa konfigurasi tambahan:

rust
use toko::harga_setelah_diskon;

#[test]
fn harga_akhir_sesuai_iklan() {
    assert_eq!(harga_setelah_diskon(200_000, 20), 160_000);
}

#[test]
fn tanpa_diskon_harga_tetap() {
    assert_eq!(harga_setelah_diskon(150_000, 0), 150_000);
}

Jalankan seluruh test dari akar proyek:

bash
cargo test

Output yang Dihasilkan:

text
   Compiling toko v0.1.0 (/tmp/w4e-integration-test/toko)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.22s
     Running unittests src/lib.rs (target/debug/deps/toko-437cac73a95d3df8)

running 1 test
test tests::hitung_diskon_dua_puluh_persen ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running tests/integration_test.rs (target/debug/deps/integration_test-3e98513de2f3ed7b)

running 2 tests
test tanpa_diskon_harga_tetap ... ok
test harga_akhir_sesuai_iklan ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests toko

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Laporan di atas memuat dua blok running N tests — itulah tanda dua suite berjalan. Mari kita bedah kode dan laporannya:

  1. Baris 1-3 (pub fn hitung_diskon(...)): fungsi pertama diberi pub agar terlihat dari luar. Unit test mengujinya langsung dari dalam crate.
  2. Baris 5-7 (pub fn harga_setelah_diskon(...)): fungsi kedua memanggil fungsi pertama — pengguna luar cukup memakai fungsi ini tanpa peduli hitung_diskon ada di baliknya.
  3. Baris 9-17 (#[cfg(test)] mod tests): unit test untuk hitung_diskon tetap di src/lib.rs, tempat detail internal diuji. Laporan blok pertama Running unittests src/lib.rs berasal dari suite ini, dan namanya tampil dengan prefiks tests::.
  4. Baris 1 file tests/integration_test.rs (use toko::harga_setelah_diskon;): import lewat nama crate toko, bukan crate:: atau super::. Posisi file ini memang di luar crate, sehingga jalur masuknya sama seperti jalur pengguna eksternal.
  5. Baris 3-6 dan 8-11 file tests/integration_test.rs: dua test memakai assert_eq! seperti biasa. Perhatikan laporan blok kedua: namanya polos (tanpa_diskon_harga_tetap, harga_akhir_sesuai_iklan) tanpa prefiks modul, karena test ini berada di akar file, bukan dalam modul tests.
  6. Baris Running tests/integration_test.rs (...): judul blok kedua menunjukkan asal file, dan binary-nya berbeda dari suite unit — integration_test-3e98513de2f3ed7b, lengkap dengan hash build-nya sendiri.

Menjalankan Hanya Suite Integration #

Saat iterasi cepat, kita bisa memilih suite mana yang dijalankan dengan opsi --test diikuti nama file tanpa akhiran .rs:

bash
cargo test --test integration_test

Output yang Dihasilkan:

text
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.00s
     Running tests/integration_test.rs (target/debug/deps/integration_test-3e98513de2f3ed7b)

running 2 tests
test harga_akhir_sesuai_iklan ... ok
test tanpa_diskon_harga_tetap ... ok

test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

Hanya blok integration yang berjalan; unit test di src/lib.rs tidak ikut dieksekusi. Sebaliknya, cargo test --lib menjalankan hanya unit test — opsi yang sudah kita lihat tercetak pada laporan kegagalan Chapter-Chapter sebelumnya. Penyaringan berlapis seperti ini membuat siklus edit-tes pada proyek besar tetap ringkas.

Batas Visibilitas: Hanya Item Publik #

Sekarang bukti bahwa integration test benar-benar berdiri di luar. Kita ubah hitung_diskon menjadi privat — hilangkan pub-nya — lalu integration test mencoba mengimpornya:

rust
fn hitung_diskon(harga: u64, persen: u64) -> u64 {
    harga * persen / 100
}

pub fn harga_setelah_diskon(harga: u64, persen: u64) -> u64 {
    harga - hitung_diskon(harga, persen)
}

File tests/integration_test.rs diganti isinya:

rust
use toko::hitung_diskon;

#[test]
fn hitung_diskon_dari_luar_crate() {
    assert_eq!(hitung_diskon(200_000, 20), 40_000);
}

Output yang Dihasilkan:

text
   Compiling toko v0.1.0 (/tmp/w4e-integration-test/toko)
error[E0603]: function `hitung_diskon` is private
 --> tests/integration_test.rs:1:11
  |
1 | use toko::hitung_diskon;
  |           ^^^^^^^^^^^^^ private function
  |
note: the function `hitung_diskon` is defined here
 --> src/lib.rs:1:1
  |
1 | fn hitung_diskon(harga: u64, persen: u64) -> u64 {
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^

For more information about this error, try `rustc --explain E0603`.
error: could not compile `toko` (test "integration_test") due to 1 previous error
warning: build failed, waiting for other jobs to finish...

Galat E0603 menolak import fungsi privat — aturan visibilitas dari Section Modul dan Visibilitas bekerja penuh di sini. Bandingkan dengan posisi unit test di Chapter Unit Test dan mod tests: mod tests di dalam crate bisa menguji fungsi privat, sedangkan file di tests/ tidak. Perbedaan ini bukan kelemahan melainkan desain: integration test sengaja dibutakan terhadap internal agar lulusnya test benar-benar berarti antarmuka publik kita bekerja, bukan sekadar detail dalam yang kebetulan benar. Bila sebuah fungsi internal layak diuji dari luar, pertimbangkan apakah fungsi tersebut memang seharusnya menjadi bagian API publik.

Ringkasan #

Integration test menempati direktori tests/ di akar proyek; setiap file di dalamnya dikompilasi sebagai crate terpisah yang mengonsumsi antarmuka pub kita lewat use nama_crate::item. Laporan cargo test menampilkan suite unit terlebih dahulu lalu tiap file integration sebagai blok running N tests tersendiri, dan opsi --test <nama> menyaring suite yang dijalankan. Hanya item publik yang terlihat dari tests/ — pelanggarannya ditolak dengan E0603, menegaskan bahwa integration test menguji dari kursi pengguna. Masih satu jenis test tersisa di cargo, yang bahkan tidak meminta kita menulis file test terpisah: contoh kode di dokumentasi. Chapter penutup section ini membahas doc test.