เพิ่ม Functionality ด้วย Test-Driven Development
ตอนนี้เรามี logic การค้นหาใน src/lib.rs แยกจากฟังก์ชัน main แล้ว
มันง่ายขึ้นมากที่จะเขียนเทสสำหรับ functionality หลักของโค้ดของเรา เรา
เรียกฟังก์ชันโดยตรงด้วยอาร์กิวเมนต์ต่าง ๆ และตรวจสอบค่า return โดยไม่
ต้องเรียก binary ของเราจาก command line ได้
ในส่วนนี้ เราจะเพิ่ม logic การค้นหาให้โปรแกรม minigrep โดยใช้
กระบวนการ test-driven development (TDD) ด้วยขั้นตอนต่อไปนี้:
- เขียนเทสที่ fail และรันมันเพื่อให้แน่ใจว่ามัน fail ด้วยเหตุผลที่ คุณคาดหวัง
- เขียนหรือแก้ไขโค้ดเพียงพอที่จะทำให้เทสใหม่ผ่าน
- Refactor โค้ดที่คุณเพิ่งเพิ่มหรือเปลี่ยน และให้แน่ใจว่าเทสยังผ่าน
- ทำซ้ำจากขั้นตอน 1!
แม้มันเป็นเพียงหนึ่งในหลายวิธีในการเขียนซอฟต์แวร์ TDD ช่วยขับเคลื่อน การออกแบบโค้ดได้ การเขียนเทสก่อนที่คุณจะเขียนโค้ดที่ทำให้เทสผ่านช่วย รักษา test coverage สูงตลอดกระบวนการ
เราจะ test-drive implementation ของ functionality ที่จะค้นหา query
string ในเนื้อหาไฟล์จริง ๆ และสร้าง list ของบรรทัดที่ตรงกับ query
เราจะเพิ่ม functionality นี้ในฟังก์ชันชื่อ search
เขียนเทสที่ Fail
ใน src/lib.rs เราจะเพิ่มโมดูล tests พร้อมฟังก์ชันเทส ดังที่เราทำใน
บทที่ 11 ฟังก์ชันเทสระบุพฤติกรรมที่เรา
ต้องการให้ฟังก์ชัน search มี — มันจะรับ query และ text ที่จะค้นหา
และมันจะ return เฉพาะบรรทัดจาก text ที่มี query Listing 12-15 แสดง
เทสนี้
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 สำหรับ functionality ที่เราอยากให้มีเทสนี้ค้นหา string "duct" text ที่เรากำลังค้นหามีสามบรรทัด มีเพียง
หนึ่งบรรทัดที่มี "duct" (สังเกตว่า backslash หลังเครื่องหมาย double
quote เปิดบอก Rust ไม่ให้ใส่ตัว newline ที่จุดเริ่มต้นของเนื้อหา
ของ string literal นี้) เรา assert ว่าค่าที่ return จากฟังก์ชัน
search มีเฉพาะบรรทัดที่เราคาดหวัง
ถ้าเรารันเทสนี้ ปัจจุบันมันจะ fail เพราะมาโคร unimplemented! panic
ด้วยข้อความ “not implemented” สอดคล้องกับหลัก TDD เราจะทำขั้นตอนเล็ก
ของการเพิ่มโค้ดเพียงพอที่จะทำให้เทสไม่ panic เมื่อเรียกฟังก์ชัน โดย
นิยามฟังก์ชัน search ให้ return vector ว่างเสมอ ดังที่แสดงใน
Listing 12-16 จากนั้น เทสควรคอมไพล์และ fail เพราะ vector ว่างไม่ตรง
กับ vector ที่บรรจุบรรทัด "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 เพียงพอที่เรียกมันจะไม่ panicตอนนี้มาพูดถึงว่าทำไมเราต้องนิยาม lifetime 'a ชัดเจนใน signature
ของ search และใช้ lifetime นั้นกับอาร์กิวเมนต์ contents และค่า
return จำได้ใน บทที่ 10 ว่า
parameter lifetime ระบุว่า lifetime ของอาร์กิวเมนต์ตัวไหนเชื่อมกับ
lifetime ของค่า return ในกรณีนี้ เราระบุว่า vector ที่ return ควร
บรรจุ string slice ที่อ้างถึง slice ของอาร์กิวเมนต์ contents (ไม่ใช่
อาร์กิวเมนต์ query)
อีกแง่หนึ่ง เราบอก Rust ว่าข้อมูลที่ return โดยฟังก์ชัน search จะ
อยู่ตราบเท่าที่ข้อมูลที่ส่งเข้าฟังก์ชัน search ในอาร์กิวเมนต์
contents สิ่งนี้สำคัญ! ข้อมูลที่อ้างถึง_โดย_ slice ต้อง valid
สำหรับ reference ที่จะ valid — ถ้า compiler สมมติเรากำลังทำ string
slice ของ query ไม่ใช่ contents มันจะทำการตรวจสอบความปลอดภัยของ
มันไม่ถูกต้อง
ถ้าเราลืม annotation lifetime และพยายามคอมไพล์ฟังก์ชันนี้ เราจะได้ error นี้:
$ 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 รู้ไม่ได้ว่า parameter ไหนในสองตัวที่เราต้องการสำหรับ output
ดังนั้นเราต้องบอกมันชัดเจน สังเกตว่า text ช่วยแนะนำให้ระบุ parameter
lifetime เดียวกันสำหรับ parameter ทั้งหมดและ output type ซึ่งไม่ถูก
ต้อง! เพราะ contents เป็น parameter ที่บรรจุ text ทั้งหมดของเรา
และเราต้องการ return ส่วนของ text นั้นที่ตรงกัน เรารู้ว่า contents
เป็น parameter เดียวที่ควรเชื่อมกับค่า return โดยใช้ lifetime syntax
ภาษาโปรแกรมอื่นไม่ต้องการให้คุณเชื่อมอาร์กิวเมนต์กับค่า return ใน signature แต่การปฏิบัตินี้จะง่ายขึ้นเมื่อเวลาผ่านไป คุณอาจต้องการ เปรียบเทียบตัวอย่างนี้กับตัวอย่างในส่วน “ตรวจสอบ Reference ด้วย Lifetime” ในบทที่ 10
เขียนโค้ดเพื่อให้เทสผ่าน
ปัจจุบัน เทสของเรา fail เพราะเรา return vector ว่างเสมอ เพื่อแก้สิ่ง
นั้นและ implement search โปรแกรมของเราต้องทำตามขั้นตอนเหล่านี้:
- iterate ผ่านแต่ละบรรทัดของเนื้อหา
- ตรวจสอบว่าบรรทัดมี query string ของเราไหม
- ถ้ามี เพิ่มมันใน list ของค่าที่เรากำลัง return
- ถ้าไม่มี ไม่ทำอะไร
- Return list ของผลที่ตรงกัน
มาทำงานผ่านแต่ละขั้นตอน เริ่มด้วย iterate ผ่านบรรทัด
Iterate ผ่านบรรทัดด้วยเมธอด lines
Rust มีเมธอดที่ช่วยจัดการ iteration แบบบรรทัดต่อบรรทัดของ string
สะดวกชื่อ lines ที่ทำงานดังที่แสดงใน Listing 12-17 สังเกตว่านี่จะ
ยังไม่คอมไพล์
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));
}
}
contentsเมธอด lines return iterator เราจะพูดถึง iterator อย่างละเอียดใน
บทที่ 13 แต่จำได้ว่าคุณเห็นวิธีใช้
iterator แบบนี้ใน Listing 3-5 ที่เราใช้
loop for พร้อม iterator เพื่อรันโค้ดบนแต่ละ item ใน collection
ค้นหาแต่ละบรรทัดสำหรับ Query
ถัดไป เราจะตรวจสอบว่าบรรทัดปัจจุบันมี query string ของเราไหม โชคดี
string มีเมธอดที่ช่วยชื่อ contains ที่ทำสิ่งนี้ให้เรา! เพิ่มการ
เรียกเมธอด contains ในฟังก์ชัน search ดังที่แสดงใน Listing 12-18
สังเกตว่านี่ยังจะไม่คอมไพล์
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));
}
}
query ไหมตอนนี้ เรากำลังสร้าง functionality เพื่อให้โค้ดคอมไพล์ เราต้อง return ค่าจาก body ดังที่เราระบุว่าเราจะทำใน signature ของฟังก์ชัน
เก็บบรรทัดที่ตรงกัน
เพื่อจบฟังก์ชันนี้ เราต้องการวิธีเก็บบรรทัดที่ตรงกันที่เราต้องการ
return สำหรับนั้น เราสร้าง mutable vector ก่อน loop for และเรียก
เมธอด push เพื่อเก็บ line ใน vector ได้ หลัง loop for เรา
return vector ดังที่แสดงใน 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));
}
}
ตอนนี้ฟังก์ชัน search ควร return เฉพาะบรรทัดที่มี query และเทส
ของเราควรผ่าน มารันเทส:
$ 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
เทสของเราผ่าน ดังนั้นเรารู้ว่ามันทำงาน!
ณ จุดนี้ เราพิจารณาโอกาสในการ refactor implementation ของฟังก์ชัน search ในขณะที่รักษาเทสให้ผ่านเพื่อรักษา functionality เดิมได้ โค้ดใน ฟังก์ชัน search ไม่แย่เกินไป แต่มันไม่ได้ใช้ประโยชน์จากฟีเจอร์ที่มี ประโยชน์ของ iterator เราจะกลับมาที่ตัวอย่างนี้ใน บทที่ 13 ที่เราจะสำรวจ iterator ใน รายละเอียด และดูวิธีปรับปรุงมัน
ตอนนี้โปรแกรมทั้งหมดควรทำงาน! ลองดู ก่อนอื่นด้วยคำที่ควร return หนึ่ง บรรทัดพอดีจากบทกวีของ 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
เจ๋ง! ตอนนี้มาลองคำที่จะตรงหลายบรรทัด เช่น 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!
และสุดท้าย มาทำให้แน่ใจว่าเราไม่ได้บรรทัดใดเมื่อค้นหาคำที่ไม่มีที่ ไหนในบทกวี เช่น 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`
ยอดเยี่ยม! เราสร้างเวอร์ชัน mini ของเครื่องมือคลาสสิกของเราเอง และ เรียนรู้มากเกี่ยวกับวิธีจัดโครงสร้าง application เรายังเรียนรู้บ้าง เกี่ยวกับ file input และ output, lifetime, การเทส และการ parse command line
เพื่อจบโปรเจกต์นี้ เราจะสาธิตสั้น ๆ ถึงวิธีทำงานกับ environment variable และวิธี print ไปยัง standard error ทั้งสองอย่างมีประโยชน์ เมื่อคุณเขียนโปรแกรม command line