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:
- Tulis sebuah pengujian yang gagal lalu jalankan buat memastikan kalau pengujian itu gagal dengan alasan yang kita harapkan.
- Tulis atau ubah kode secukupnya saja buat bikin pengujian baru itu sukses.
- Refactor (rombak) kode yang baru saja ditambahkan atau diubah dan pastikan pengujiannya tetap sukses.
- 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.
Menulis Pengujian yang Gagal
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.
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));
}
}
search untuk fungsionalitas yang kita harapkan adaPengujian 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."
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));
}
}
search secukupnya saja sehingga ia tidak panic saat dipanggilSekarang 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.
Menulis Kode Agar Pengujian Sukses
Saat ini, pengujian kita gagal karena kita selalu mengembalikan vector kosong.
Buat memperbaikinya dan mengimplementasikan search, program kita harus mengikuti
langkah-langkah berikut:
- Iterasi melewati setiap baris dari konten.
- Mengecek apakah baris tersebut mengandung string kueri kita.
- Jika iya, tambahkan baris itu ke daftar nilai yang bakal kita kembalikan.
- Jika tidak, jangan lakukan apa-apa.
- 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.
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));
}
}
contentsMethod 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.
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));
}
}
querySaat 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.
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));
}
}
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.