Integration Test

Direktori tests/ dan pengujian API publik.

Daftar isi

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.

Aspekunit testintegration test
lokasimodul #[cfg(test)] di dalam src/berkas pada direktori tests/
posisi terhadap kodeberada di dalam cratecrate terpisah pemakai pustaka
yang terlihatseluruh item termasuk privathanya API publik
kompilasi saat build biasatidak ikuttidak ikut
sasaran yang cocokdetail implementasikontrak 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.

text Struktur direktori proyek penghitung-kata
penghitung-kata/
├── Cargo.toml
├── src/
│   └── lib.rs
└── tests/
    ├── api_publik.rs
    └── common/
        └── mod.rs
toml Cargo.toml
[package]
name = "penghitung-kata"
version = "0.1.0"
edition = "2021"
rust src/lib.rs
// 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");
    }
}
rust tests/api_publik.rs
// 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);
}
rust tests/common/mod.rs
// 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.

crate pustakapenghitung_katapub fn hitung_katapub fn kata_terpanjangfn rapikan (privat)use API publiktidak terlihatdari luar cratecrate pengujiantests/api_publik.rssatu berkas, satu biner ujimod common sebagai pembantu
Batas antara crate pustaka dan crate pengujian

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.

bash
# 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.rs

Menjalankan 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.

bash
# jalankan seluruh target uji berurutan agar urutan baris stabil
cargo test --manifest-path /tmp/penghitung-uji/Cargo.toml -- --test-threads=1 2>/dev/null
text Keluaran
running 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.00s

Keluaran 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.

bash
# jalankan hanya target uji api_publik
cargo test --manifest-path /tmp/penghitung-uji/Cargo.toml --test api_publik -- --test-threads=1 2>/dev/null
text Keluaran
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

Pemilihan 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 .rs tingkat atas pada tests/ menjadi crate tersendiri yang memakai pustaka sebagai pemakai luar, sehingga hanya API publik yang terlihat.
  • cargo test menjalankan unit test, integration test, dan doc test sebagai bagian keluaran terpisah, serta opsi --test nama menjalankan satu target.
  • Pembantu bersama ditempatkan pada tests/common/mod.rs dan dideklarasikan dengan mod common; karena setiap berkas pengujian adalah akar crate.
  • Integration test yang canggung adalah sinyal desain API yang kurang nyaman bagi pemakai luar.

Referensi#