Topik ini membahas integration test, yaitu pengujian yang memakai pustaka sebagaimana pemakai luar: melalui API publiknya tanpa hak istimewa melihat bagian dalam. Pembahasan mencakup peran direktori tests/, pembentukan setiap berkas pengujian sebagai crate tersendiri, penggunaan pembantu bersama antar berkas, serta pemilihan target uji pada perintah cargo test. Contoh memakai proyek penghitung-kata yang dijalankan sungguhan lengkap dengan unit test di dalam pustakanya. Setelah membacanya, pengujian tingkat publik dapat dipisahkan dari unit test, dan batas antara keduanya dapat ditetapkan dengan alasan yang jelas.
Posisi integration test di dalam proyek#
Integration test menjawab pertanyaan yang tidak dapat dijawab unit test, yaitu bagaimana crate dipakai oleh kode luar. Setiap berkas Rust pada tingkat atas direktori tests/ dikompilasi menjadi crate tersendiri yang menautkan pustaka sebagai dependensi, persis seperti crate pihak ketiga mana pun. Konsekuensinya bersifat dua arah: hanya item pub yang terlihat, dan pengerjaan API tidak lagi dapat mengandalkan trik internal seperti akses fungsi privat.
| Aspek | unit test | integration test |
|---|---|---|
| lokasi | modul #[cfg(test)] di dalam src/ | berkas pada direktori tests/ |
| posisi terhadap kode | berada di dalam crate | crate terpisah pemakai pustaka |
| yang terlihat | seluruh item termasuk privat | hanya API publik |
| kompilasi saat build biasa | tidak ikut | tidak ikut |
| sasaran yang cocok | detail implementasi | kontrak API dari sisi pemanggil |
Pemisahan ini juga memberi sinyal desain. Fungsi yang pengujian integrasinya terasa canggung biasanya menandakan API yang kurang nyaman dipakai, sebab integration test menjadi pemakai pertama API tersebut. Sebaliknya, pengujian detail kecil yang hanya memakan waktu bila ditulis dari luar sebaiknya tetap menjadi unit test.
Proyek contoh dan direktori tests/#
Proyek penghitung-kata memuat pustaka dengan dua fungsi publik, satu fungsi privat bernama rapikan, satu unit test untuk fungsi privat itu, serta satu berkas integration test yang memakai pembantu bersama. Struktur direktorinya mengikuti konvensi Cargo.
penghitung-kata/
├── Cargo.toml
├── src/
│ └── lib.rs
└── tests/
├── api_publik.rs
└── common/
└── mod.rs[package]
name = "penghitung-kata"
version = "0.1.0"
edition = "2021"// tidak-dijalankan sendirian oleh rustc karena berkas pustaka tanpa fungsi main.
//! Penghitung kata untuk teks bahasa Indonesia.
/// Menghitung banyaknya kata setelah spasi beruntun dirapikan.
pub fn hitung_kata(teks: &str) -> usize {
rapikan(teks).split_whitespace().count()
}
/// Mengembalikan kata terpanjang pada teks, bila ada.
pub fn kata_terpanjang(teks: &str) -> Option<String> {
rapikan(teks)
.split_whitespace()
.max_by_key(|kata| kata.len())
.map(|kata| kata.to_string())
}
/// Memangkas spasi di awal, di akhir, dan yang beruntun di tengah teks.
fn rapikan(teks: &str) -> String {
teks.split_whitespace().collect::<Vec<_>>().join(" ")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn rapikan_menyatukan_spasi() {
assert_eq!(rapikan(" belajar rust "), "belajar rust");
}
}// tidak-dijalankan sendirian oleh rustc karena memakai crate penghitung_kata sebagai pustaka eksternal.
//! Pengujian integrasi atas API publik penghitung-kata.
mod common;
use penghitung_kata::{hitung_kata, kata_terpanjang};
#[test]
fn hitung_teks_contoh() {
assert_eq!(hitung_kata(common::teks_contoh()), 9);
}
#[test]
fn hitung_spasi_beruntun() {
assert_eq!(hitung_kata(" belajar rust "), 2);
}
#[test]
fn terpanjang_dari_teks_contoh() {
assert_eq!(
kata_terpanjang(common::teks_contoh()).as_deref(),
Some("penghitung")
);
}
#[test]
fn terpanjang_teks_kosong() {
assert_eq!(kata_terpanjang(""), None);
}// tidak-dijalankan sendirian oleh rustc karena modul pembantu tanpa fungsi main.
//! Pembantu bersama untuk berkas pengujian integrasi.
/// Teks contoh dengan sembilan kata.
pub fn teks_contoh() -> &'static str {
"penghitung kata sederhana untuk latihan integrasi pada jalur rust"
}Baris use penghitung_kata::... menegaskan posisi berkas tests/api_publik.rs sebagai pemakai luar, dan fungsi rapikan yang tidak bertanda pub tidak dapat dipanggil dari sana. Diagram berikut memetakan batas itu: dua fungsi publik menjadi jembatan satu-satunya, sedangkan fungsi privat hanya terlihat oleh unit test di dalam pustaka.
Eksekusi contoh memakai proyek di luar pohon konten situs. Direktori tests/common dibuat lebih dahulu karena cargo new hanya menyiapkan src/, lalu seluruh berkas disalin dari bundle topik.
# siapkan proyek percobaan yang bersih di luar pohon konten
rm -rf /tmp/penghitung-uji
cargo new /tmp/penghitung-uji --lib -q --vcs none
# siapkan direktori tests beserta subdirektorinya
mkdir -p /tmp/penghitung-uji/tests/common
# salin berkas proyek dari bundle ke posisinya masing-masing
cp Cargo.toml /tmp/penghitung-uji/Cargo.toml
cp src/lib.rs /tmp/penghitung-uji/src/lib.rs
cp tests/api_publik.rs /tmp/penghitung-uji/tests/api_publik.rs
cp tests/common/mod.rs /tmp/penghitung-uji/tests/common/mod.rsMenjalankan integration test bersama unit test#
Perintah cargo test menjalankan seluruh target uji secara berurutan dan melaporkannya dalam bagian terpisah. Opsi --test-threads=1 serta pembuangan stderr dipakai agar keluaran contoh stabil, seperti yang dijelaskan pada topik unit test.
# jalankan seluruh target uji berurutan agar urutan baris stabil
cargo test --manifest-path /tmp/penghitung-uji/Cargo.toml -- --test-threads=1 2>/dev/nullrunning 1 test
test tests::rapikan_menyatukan_spasi ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 4 tests
test hitung_spasi_beruntun ... ok
test hitung_teks_contoh ... ok
test terpanjang_dari_teks_contoh ... ok
test terpanjang_teks_kosong ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sKeluaran memuat tiga bagian yang mewakili tiga target: unit test pada pustaka, integration test pada tests/api_publik.rs, dan doc test yang masih kosong. Nama pengujian pada bagian kedua tidak memuat awalan tests:: karena fungsi itu berada pada akar crate pengujian, bukan pada modul bernama tests. Satu target juga dapat dijalankan sendirian dengan opsi --test yang menerima nama berkas tanpa akhiran .rs.
# jalankan hanya target uji api_publik
cargo test --manifest-path /tmp/penghitung-uji/Cargo.toml --test api_publik -- --test-threads=1 2>/dev/nullrunning 4 tests
test hitung_spasi_beruntun ... ok
test hitung_teks_contoh ... ok
test terpanjang_dari_teks_contoh ... ok
test terpanjang_teks_kosong ... ok
test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00sPemilihan target berguna ketika integration test memakan waktu kompilasi lebih lama daripada unit test, karena setiap berkas pada tests/ menjadi crate utuh yang dikompilasi terpisah dari berkas lain. Opsi --lib bekerja sebaliknya dengan hanya menjalankan unit test pada src/.
Pembantu bersama antar berkas#
Berkas tests/common/mod.rs menjadi modul bersama lewat deklarasi mod common; pada berkas pengujian. Deklarasi itu berhasil karena setiap berkas tests/*.rs adalah akar crate, sehingga pencarian modul mengarah ke tests/common.rs atau tests/common/mod.rs. Direktori bersarang seperti common/ tidak menjadi target uji sendiri karena Cargo hanya menjadikan berkas pada tingkat atas tests/ sebagai target.
Pembantu pada modul itu menghindari penyalinan teks contoh yang sama ke banyak berkas, sehingga perubahan pada satu tempat langsung berlaku untuk seluruh pengujian yang memakainya. Isi modul tetap berupa kode biasa tanpa #[cfg(test)], karena seluruh berkas pada tests/ memang hanya dikompilasi saat pengujian. Pola yang sama dapat menyimpan prosedur persiapan yang panjang, misalnya pembuatan berkas sementara atau penyusunan struktur data uji.
Kesalahan umum#
Mengimpor fungsi privat dari tests/. Deklarasi use penghitung_kata::rapikan; gagal dikompilasi karena rapikan tidak bertanda pub. Bila detail internal memang perlu diuji dari luar, ubah desainnya menjadi API publik yang layak, atau pindahkan pengujian tersebut menjadi unit test di dalam pustaka.
Menaruh pembantu sebagai berkas tingkat atas. Berkas tests/common.rs akan dianggap sebagai target uji sendiri dan menghasilkan bagian running 0 tests pada keluaran. Letakkan pembantu pada tests/common/mod.rs agar tidak menjadi target.
Membiarkan proyek hanya berisi biner. Berkas pada tests/ menautkan crate pustaka, sehingga paket yang hanya memuat src/main.rs tidak dapat diimpor oleh integration test. Pindahkan logika ke src/lib.rs dan jadikan main.rs pembungkus tipis, sebagaimana pola pada modul tata letak proyek.
Ringkasan#
- Setiap berkas
.rstingkat atas padatests/menjadi crate tersendiri yang memakai pustaka sebagai pemakai luar, sehingga hanya API publik yang terlihat. cargo testmenjalankan unit test, integration test, dan doc test sebagai bagian keluaran terpisah, serta opsi--test namamenjalankan satu target.- Pembantu bersama ditempatkan pada
tests/common/mod.rsdan dideklarasikan denganmod common;karena setiap berkas pengujian adalah akar crate. - Integration test yang canggung adalah sinyal desain API yang kurang nyaman bagi pemakai luar.
Referensi#
- Test Organization — The Rust Book — Pembagian unit test dan integration test pada Rust.
- Package Layout — The Cargo Book
— Peran direktori
tests/dalam struktur proyek.