Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Mengembangkan Fungsionalitas Library dengan Test-Driven Development

Sekarang setelah logika pencarian kita terpisah di src/lib.rs dari fungsi main, jauh lebih mudah untuk menulis pengujian buat fungsionalitas inti dari kode kita. Kita bisa memanggil fungsi secara langsung dengan berbagai argumen dan mengecek nilai kembaliannya tanpa perlu menjalankan binary kita dari command line.

Di bagian ini, kita bakal menambahkan logika pencarian ke program minigrep memakai proses test-driven development (TDD) dengan langkah-langkah berikut:

  1. Tulis sebuah pengujian yang gagal lalu jalankan buat memastikan kalau pengujian itu gagal dengan alasan yang kita harapkan.
  2. Tulis atau ubah kode secukupnya saja buat bikin pengujian baru itu sukses.
  3. Refactor (rombak) kode yang baru saja ditambahkan atau diubah dan pastikan pengujiannya tetap sukses.
  4. Ulangi lagi dari langkah 1!

Meskipun ini cuma salah satu dari sekian banyak cara buat nulis software, TDD bisa membantu mengarahkan desain kode. Menulis pengujian sebelum kita menulis kode yang bakal membuat pengujian itu sukses membantu mempertahankan test coverage (cakupan pengujian) yang tinggi di sepanjang proses.

Kita bakal menguji-kembangkan (test-drive) implementasi fungsionalitas yang nantinya bakal benar-benar melakukan pencarian string kueri di dalam isi file lalu menghasilkan daftar baris yang cocok dengan kueri tersebut. Kita bakal menambahkan fungsionalitas ini ke dalam sebuah fungsi bernama search.

Di src/lib.rs, kita bakal menambahkan modul tests dengan fungsi pengujiannya, seperti yang kita lakukan di Bab 11. Fungsi pengujian ini menentukan perilaku yang kita inginkan dari fungsi search: fungsi ini bakal menerima sebuah kueri dan teks tempat kita mencari, lalu ia cuma bakal mengembalikan baris-baris dari teks tersebut yang mengandung kuerinya. Listing 12-15 menunjukkan pengujian ini.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    unimplemented!();
}

// --snip--

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-15: Membuat pengujian yang gagal buat fungsi search untuk fungsionalitas yang kita harapkan ada

Pengujian ini mencari string "duct". Teks yang lagi kita cari ada tiga baris, dan cuma satu yang mengandung "duct" (perhatikan bahwa tanda backslash setelah tanda kutip ganda pembuka memberi tahu Rust buat tidak menambahkan karakter newline alias baris baru di awal konten string literal ini). Kita menegaskan bahwa nilai yang dikembalikan oleh fungsi search cuma berisi baris yang kita harapkan.

Kalau kita menjalankan pengujian ini sekarang, ia bakal gagal karena macro unimplemented! mengalami panic dengan pesan “not implemented”. Mengikuti prinsip-prinsip TDD, kita bakal mengambil langkah kecil dengan menambahkan kode secukupnya agar pengujian ini tidak panic saat memanggil fungsi tersebut; kita lakukan dengan mendefinisikan fungsi search agar selalu mengembalikan vector kosong, seperti yang ditunjukkan di Listing 12-16. Kemudian pengujian ini seharusnya berhasil di-compile dan lalu gagal karena vector kosong tidak cocok dengan vector yang isinya baris "safe, fast, productive."

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    vec![]
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-16: Mendefinisikan fungsi search secukupnya saja sehingga ia tidak panic saat dipanggil

Sekarang mari kita bahas kenapa kita perlu mendefinisikan lifetime 'a secara eksplisit di signature fungsi search dan memakai lifetime itu bersama argumen contents dan nilai kembaliannya. Ingat kembali di Bab 10 bahwa parameter lifetime menentukan lifetime argumen mana yang terhubung dengan lifetime nilai kembaliannya. Di kasus ini, kita mengindikasikan kalau vector yang dikembalikan seharusnya berisi string slices yang merujuk pada slices dari argumen contents (bukannya dari argumen query).

Dengan kata lain, kita memberi tahu Rust kalau data yang dikembalikan oleh fungsi search bakal hidup (live) selama data yang diteruskan ke dalam fungsi search lewat argumen contents. Ini penting! Data yang dirujuk oleh sebuah slice harus tetap valid agar referensinya juga ikut valid; kalau compiler mengasumsikan kalau kita lagi bikin string slices dari query bukannya contents, pengecekan keamanannya bakal dilakukan dengan tidak tepat.

Kalau kita kelupaan menganotasi lifetime dan mencoba men-compile fungsi ini, kita bakal dapat error ini:

$ cargo build
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
error[E0106]: missing lifetime specifier
 --> src/lib.rs:1:51
  |
1 | pub fn search(query: &str, contents: &str) -> Vec<&str> {
  |                      ----            ----         ^ expected named lifetime parameter
  |
  = help: this function's return type contains a borrowed value, but the signature does not say whether it is borrowed from `query` or `contents`
help: consider introducing a named lifetime parameter
  |
1 | pub fn search<'a>(query: &'a str, contents: &'a str) -> Vec<&'a str> {
  |              ++++         ++                 ++              ++

For more information about this error, try `rustc --explain E0106`.
error: could not compile `minigrep` (lib) due to 1 previous error

Rust tidak bisa tahu pasti dari kedua parameter ini mana yang kita butuhkan buat outputnya, jadi kita harus memberitahukannya secara eksplisit. Perhatikan bahwa teks bantuannya menyarankan buat menentukan parameter lifetime yang sama buat semua parameter beserta tipe outputnya, yang mana itu salah! Karena contents adalah parameter yang berisi semua teks kita dan kita mau mengembalikan bagian-bagian dari teks itu yang cocok, kita jadi tahu kalau cuma parameter contents yang seharusnya dihubungkan dengan nilai kembaliannya menggunakan sintaks lifetime.

Bahasa pemrograman lain biasanya tidak mengharuskan kita buat menghubungkan argumen dengan nilai kembalian di signature-nya, tapi praktik ini bakal terasa lebih mudah seiring berjalannya waktu. Kita mungkin mau membandingkan contoh ini dengan contoh-contoh di bagian “Memvalidasi Referensi dengan Lifetimes” di Bab 10.

Saat ini, pengujian kita gagal karena kita selalu mengembalikan vector kosong. Buat memperbaikinya dan mengimplementasikan search, program kita harus mengikuti langkah-langkah berikut:

  1. Iterasi melewati setiap baris dari konten.
  2. Mengecek apakah baris tersebut mengandung string kueri kita.
  3. Jika iya, tambahkan baris itu ke daftar nilai yang bakal kita kembalikan.
  4. Jika tidak, jangan lakukan apa-apa.
  5. Kembalikan daftar hasil yang cocok.

Mari kerjakan satu per satu setiap langkahnya, dimulai dari iterasi melewati baris-baris.

Iterasi Melewati Baris dengan Method lines

Rust punya method yang membantu sekali buat menangani iterasi baris per baris dari strings, dinamai dengan nama yang nyaman yaitu lines, yang bekerja seperti yang ditunjukkan di Listing 12-17. Perhatikan kalau kode ini belum bisa di-compile.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        // do something with line
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-17: Iterasi melewati tiap baris di contents

Method lines mengembalikan sebuah iterator. Kita bakal membahas iterator lebih mendalam di Bab 13, tapi ingat kembali bahwa kita pernah melihat cara memakai iterator seperti ini di Listing 3-5, di mana kita memakai for loop dengan sebuah iterator buat menjalankan beberapa kode pada setiap item di dalam sebuah collection (koleksi).

Mencari Kueri di Setiap Baris

Berikutnya, kita bakal mengecek apakah baris saat ini mengandung string kueri kita. Untungnya, tipe string punya method pembantu (helper) bernama contains yang ngelakuin persis apa yang kita butuhkan! Tambahkan pemanggilan ke method contains di dalam fungsi search, seperti yang ditunjukkan di Listing 12-18. Perhatikan kalau kode ini masih belum bisa di-compile.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    for line in contents.lines() {
        if line.contains(query) {
            // do something with line
        }
    }
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-18: Menambahkan fungsionalitas buat ngecek apakah barisnya mengandung string di query

Saat ini, kita baru membangun fungsionalitasnya. Supaya kodenya bisa di-compile, kita perlu mengembalikan sebuah nilai dari body fungsi sesuai indikasi yang kita berikan di signature fungsi.

Menyimpan Baris yang Cocok

Buat menyelesaikan fungsi ini, kita butuh cara buat menyimpan baris-baris yang cocok yang mau kita kembalikan. Buat melakukan itu, kita bisa membuat vector mutable sebelum for loop lalu memanggil method push buat menyimpan line ke dalam vector tersebut. Setelah for loop selesai, kita mengembalikan vector-nya, seperti yang ditunjukkan di Listing 12-19.

Filename: src/lib.rs
pub fn search<'a>(query: &str, contents: &'a str) -> Vec<&'a str> {
    let mut results = Vec::new();

    for line in contents.lines() {
        if line.contains(query) {
            results.push(line);
        }
    }

    results
}

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

    #[test]
    fn one_result() {
        let query = "duct";
        let contents = "\
Rust:
safe, fast, productive.
Pick three.";

        assert_eq!(vec!["safe, fast, productive."], search(query, contents));
    }
}
Listing 12-19: Menyimpan baris-baris yang cocok supaya kita bisa mengembalikannya

Sekarang fungsi search seharusnya hanya mengembalikan baris-baris yang mengandung query, dan pengujian kita seharusnya sukses. Mari kita jalankan pengujiannya:

$ cargo test
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 1.22s
     Running unittests src/lib.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 1 test
test tests::one_result ... ok

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

     Running unittests src/main.rs (target/debug/deps/minigrep-9cd200e5fac0fc94)

running 0 tests

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

   Doc-tests minigrep

running 0 tests

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

Pengujian kita sukses, jadi kita tahu kalau ini berfungsi!

Di titik ini, kita bisa mempertimbangkan peluang buat me-refactor implementasi dari fungsi pencarian sambil menjaga agar pengujiannya tetap sukses demi mempertahankan fungsionalitas yang sama. Kode di fungsi pencariannya tidak terlalu jelek sih, tapi dia tidak memanfaatkan beberapa fitur iterator yang sangat berguna. Kita bakal kembali lagi ke contoh ini di Bab 13, di mana kita bakal mengeksplorasi iterator lebih dalam, dan melihat gimana cara meningkatkannya.

Sekarang keseluruhan programnya seharusnya sudah bisa jalan! Mari kita coba, pertama dengan kata yang seharusnya cuma mengembalikan satu baris persis dari puisi Emily Dickinson: frog.

$ cargo run -- frog poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/minigrep frog poem.txt`
How public, like a frog

Keren! Sekarang mari kita coba pakai kata yang bakal cocok dengan beberapa baris sekaligus, misalnya body:

$ cargo run -- body poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep body poem.txt`
I'm nobody! Who are you?
Are you nobody, too?
How dreary to be somebody!

Dan terakhir, mari kita pastikan kalau kita tidak dapat baris apa pun ketika kita mencari kata yang sama sekali tidak ada di dalam puisinya, misalnya monomorphization:

$ cargo run -- monomorphization poem.txt
   Compiling minigrep v0.1.0 (file:///projects/minigrep)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.0s
     Running `target/debug/minigrep monomorphization poem.txt`

Luar biasa! Kita sudah membangun versi mini kita sendiri dari alat klasik dan belajar banyak hal soal cara menata aplikasi. Kita juga belajar sedikit tentang input dan output file, lifetimes, pengujian, dan penguraian command line.

Buat melengkapi project ini, kita bakal mendemonstrasikan secara singkat gimana cara berurusan dengan environment variables (variabel lingkungan) dan gimana cara mencetak pesan ke standard error, yang mana keduanya sangat berguna pas kita lagi nulis program command line.