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

เพิ่ม Functionality ด้วย Test-Driven Development

ตอนนี้เรามี logic การค้นหาใน src/lib.rs แยกจากฟังก์ชัน main แล้ว มันง่ายขึ้นมากที่จะเขียนเทสสำหรับ functionality หลักของโค้ดของเรา เรา เรียกฟังก์ชันโดยตรงด้วยอาร์กิวเมนต์ต่าง ๆ และตรวจสอบค่า return โดยไม่ ต้องเรียก binary ของเราจาก command line ได้

ในส่วนนี้ เราจะเพิ่ม logic การค้นหาให้โปรแกรม minigrep โดยใช้ กระบวนการ test-driven development (TDD) ด้วยขั้นตอนต่อไปนี้:

  1. เขียนเทสที่ fail และรันมันเพื่อให้แน่ใจว่ามัน fail ด้วยเหตุผลที่ คุณคาดหวัง
  2. เขียนหรือแก้ไขโค้ดเพียงพอที่จะทำให้เทสใหม่ผ่าน
  3. Refactor โค้ดที่คุณเพิ่งเพิ่มหรือเปลี่ยน และให้แน่ใจว่าเทสยังผ่าน
  4. ทำซ้ำจากขั้นตอน 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 แสดง เทสนี้

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: สร้างเทสที่ fail สำหรับฟังก์ชัน 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."

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: นิยามฟังก์ชัน 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 โปรแกรมของเราต้องทำตามขั้นตอนเหล่านี้:

  1. iterate ผ่านแต่ละบรรทัดของเนื้อหา
  2. ตรวจสอบว่าบรรทัดมี query string ของเราไหม
  3. ถ้ามี เพิ่มมันใน list ของค่าที่เรากำลัง return
  4. ถ้าไม่มี ไม่ทำอะไร
  5. Return list ของผลที่ตรงกัน

มาทำงานผ่านแต่ละขั้นตอน เริ่มด้วย iterate ผ่านบรรทัด

Iterate ผ่านบรรทัดด้วยเมธอด lines

Rust มีเมธอดที่ช่วยจัดการ iteration แบบบรรทัดต่อบรรทัดของ string สะดวกชื่อ lines ที่ทำงานดังที่แสดงใน Listing 12-17 สังเกตว่านี่จะ ยังไม่คอมไพล์

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: iterate ผ่านแต่ละบรรทัดใน 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 สังเกตว่านี่ยังจะไม่คอมไพล์

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: เพิ่ม functionality เพื่อดูว่าบรรทัดมี string ใน query ไหม

ตอนนี้ เรากำลังสร้าง functionality เพื่อให้โค้ดคอมไพล์ เราต้อง return ค่าจาก body ดังที่เราระบุว่าเราจะทำใน signature ของฟังก์ชัน

เก็บบรรทัดที่ตรงกัน

เพื่อจบฟังก์ชันนี้ เราต้องการวิธีเก็บบรรทัดที่ตรงกันที่เราต้องการ return สำหรับนั้น เราสร้าง mutable vector ก่อน loop for และเรียก เมธอด push เพื่อเก็บ line ใน vector ได้ หลัง loop for เรา return vector ดังที่แสดงใน 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: เก็บบรรทัดที่ตรงกันเพื่อให้เรา return พวกมันได้

ตอนนี้ฟังก์ชัน 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