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

การจัดระเบียบเทส

ดังที่กล่าวไว้ตอนต้นบท การเทสเป็นวินัยที่ซับซ้อน และคนต่างกันใช้คำศัพท์ และการจัดระเบียบต่างกัน community Rust คิดถึงเทสในแง่ของสองหมวดหลัก — unit test และ integration test unit test มีขนาดเล็กและโฟกัสมากกว่า ทดสอบโมดูลหนึ่งโดยแยกออกในแต่ละครั้ง และทดสอบ interface ส่วนตัวได้ integration test อยู่ภายนอก library ของคุณทั้งหมด และใช้โค้ดของคุณใน แบบเดียวกับโค้ดภายนอกอื่นใด โดยใช้เฉพาะ public interface และอาจใช้งาน หลายโมดูลต่อเทส

การเขียนเทสทั้งสองประเภทสำคัญในการทำให้แน่ใจว่าชิ้นส่วนของ library ของ คุณทำสิ่งที่คุณคาดหวัง ทั้งแยกกันและด้วยกัน

Unit Test

จุดประสงค์ของ unit test คือทดสอบแต่ละหน่วยของโค้ดโดยแยกจากโค้ดส่วนที่ เหลือ เพื่อระบุได้รวดเร็วว่าโค้ดทำงานและไม่ทำงานตามที่คาดหวังที่ไหน คุณจะวาง unit test ใน directory src ในแต่ละไฟล์พร้อมโค้ดที่พวกมัน ทดสอบ ธรรมเนียมคือสร้างโมดูลชื่อ tests ในแต่ละไฟล์เพื่อบรรจุฟังก์ชัน เทส และ annotate โมดูลด้วย cfg(test)

โมดูล tests และ #[cfg(test)]

annotation #[cfg(test)] บนโมดูล tests บอก Rust ให้คอมไพล์และรัน โค้ดเทสเฉพาะเมื่อคุณรัน cargo test ไม่ใช่เมื่อคุณรัน cargo build นี่ประหยัดเวลาคอมไพล์เมื่อคุณต้องการ build เฉพาะ library และประหยัด พื้นที่ใน artifact ที่ถูกคอมไพล์เพราะเทสไม่ถูกรวม คุณจะเห็นว่าเพราะ integration test ไปอยู่ใน directory ที่ต่างกัน พวกมันไม่ต้องการ annotation #[cfg(test)] อย่างไรก็ตาม เพราะ unit test ไปอยู่ในไฟล์ เดียวกับโค้ด คุณจะใช้ #[cfg(test)] เพื่อระบุว่าพวกมันไม่ควรถูกรวม ในผลที่คอมไพล์

จำได้ว่าเมื่อเราสร้างโปรเจกต์ adder ใหม่ในส่วนแรกของบทนี้ Cargo สร้าง โค้ดนี้ให้เรา:

Filename: src/lib.rs

pub fn add(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

บนโมดูล tests ที่ถูกสร้างอัตโนมัติ attribute cfg ย่อมาจาก configuration และบอก Rust ว่า item ต่อไปนี้ควรถูกรวมเฉพาะเมื่อให้ configuration option บางอย่าง ในกรณีนี้ configuration option คือ test ซึ่ง Rust ให้มาสำหรับการคอมไพล์และรันเทส โดยใช้ attribute cfg Cargo คอมไพล์โค้ดเทสของเราเฉพาะถ้าเราเรียกใช้รันเทสด้วย cargo test อย่างจริงจัง สิ่งนี้รวมฟังก์ชัน helper ใด ๆ ที่อาจอยู่ในโมดูลนี้ นอก เหนือจากฟังก์ชันที่ annotate ด้วย #[test]

ทดสอบฟังก์ชัน Private

มีการถกเถียงใน community การเทสว่าควรหรือไม่ควรทดสอบฟังก์ชัน private โดยตรง และภาษาอื่นทำให้ยากหรือเป็นไปไม่ได้ที่จะทดสอบฟังก์ชัน private ไม่ว่าคุณยึดถืออุดมการณ์การเทสไหน กฎ privacy ของ Rust อนุญาตให้คุณ ทดสอบฟังก์ชัน private ได้ พิจารณาโค้ดใน Listing 11-12 กับฟังก์ชัน private internal_adder

Filename: src/lib.rs
pub fn add_two(a: u64) -> u64 {
    internal_adder(a, 2)
}

fn internal_adder(left: u64, right: u64) -> u64 {
    left + right
}

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

    #[test]
    fn internal() {
        let result = internal_adder(2, 2);
        assert_eq!(result, 4);
    }
}
Listing 11-12: ทดสอบฟังก์ชัน private

สังเกตว่าฟังก์ชัน internal_adder ไม่ถูกทำเครื่องหมายเป็น pub เทส เป็นแค่โค้ด Rust และโมดูล tests เป็นแค่โมดูลอื่น ดังที่เราพูดถึงใน “Path สำหรับอ้างถึง item ใน Module Tree” item ในโมดูลลูกใช้ item ในโมดูลบรรพบุรุษของพวกมันได้ ในเทสนี้ เรานำ item ทั้งหมดที่เป็นของ parent ของโมดูล tests เข้า scope ด้วย use super::* และเทสเรียก internal_adder ได้ ถ้าคุณไม่คิดว่าฟังก์ชัน private ควรถูกทดสอบ ไม่มีอะไรใน Rust ที่จะบังคับให้คุณทำ

Integration Test

ใน Rust integration test อยู่ภายนอก library ของคุณทั้งหมด พวกมันใช้ library ของคุณในแบบเดียวกับโค้ดอื่นใด แปลว่าพวกมันเรียกได้เฉพาะ ฟังก์ชันที่เป็นส่วนหนึ่งของ public API ของ library คุณ จุดประสงค์ ของพวกมันคือทดสอบว่าหลายส่วนของ library ทำงานร่วมกันถูกต้องไหม หน่วย ของโค้ดที่ทำงานถูกต้องด้วยตัวเองอาจมีปัญหาเมื่อ integrate ดังนั้น test coverage ของโค้ดที่ integrate ก็สำคัญด้วย ในการสร้าง integration test ก่อนอื่นคุณต้องการ directory tests

Directory tests

เราสร้าง directory tests ที่ระดับ top ของ directory โปรเจกต์ของเรา ถัดจาก src Cargo รู้ว่าต้องมองหาไฟล์ integration test ใน directory นี้ เราสร้างไฟล์เทสได้มากเท่าที่ต้องการ และ Cargo จะคอมไพล์แต่ละไฟล์ เป็น crate แยก

มาสร้าง integration test กัน โดยให้โค้ดใน Listing 11-12 ยังอยู่ใน ไฟล์ src/lib.rs สร้าง directory tests และสร้างไฟล์ใหม่ชื่อ tests/integration_test.rs โครงสร้าง directory ของคุณควรดูแบบนี้:

adder
├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    └── integration_test.rs

ใส่โค้ดใน Listing 11-13 ในไฟล์ tests/integration_test.rs

Filename: tests/integration_test.rs
use adder::add_two;

#[test]
fn it_adds_two() {
    let result = add_two(2);
    assert_eq!(result, 4);
}
Listing 11-13: integration test ของฟังก์ชันใน crate adder

แต่ละไฟล์ใน directory tests เป็น crate แยก ดังนั้นเราต้องนำ library ของเราเข้า scope ของแต่ละ crate เทส ด้วยเหตุผลนั้น เราเพิ่ม use adder::add_two; ที่บนสุดของโค้ด ซึ่งเราไม่ต้องการใน unit test

เราไม่ต้อง annotate โค้ดใน tests/integration_test.rs ด้วย #[cfg(test)] Cargo ปฏิบัติกับ directory tests เป็นพิเศษและคอมไพล์ ไฟล์ใน directory นี้เฉพาะเมื่อเรารัน cargo test รัน cargo test ตอนนี้:

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

running 1 test
test tests::internal ... ok

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-1082c4b063a8fbe6)

running 1 test
test it_adds_two ... ok

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

   Doc-tests adder

running 0 tests

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

สามส่วนของ output รวม unit test, integration test และ doc test สังเกตว่าถ้าเทสใดในส่วนหนึ่ง fail ส่วนถัดไปจะไม่ถูกรัน ตัวอย่างเช่น ถ้า unit test fail จะไม่มี output สำหรับ integration และ doc test เพราะเทสเหล่านั้นจะถูกรันเฉพาะถ้า unit test ทั้งหมดผ่าน

ส่วนแรกสำหรับ unit test เหมือนที่เราเห็นมา — หนึ่งบรรทัดสำหรับแต่ละ unit test (อันหนึ่งชื่อ internal ที่เราเพิ่มใน Listing 11-12) แล้ว บรรทัดสรุปสำหรับ unit test

ส่วน integration test เริ่มด้วยบรรทัด Running tests/integration_test.rs ถัดไปคือบรรทัดสำหรับแต่ละฟังก์ชัน เทสใน integration test นั้น และบรรทัดสรุปสำหรับผลของ integration test ก่อนที่ส่วน Doc-tests adder จะเริ่ม

ไฟล์ integration test แต่ละไฟล์มีส่วนของตัวเอง ดังนั้นถ้าเราเพิ่มไฟล์ อีกใน directory tests จะมีส่วน integration test อีก

เรายังรันฟังก์ชัน integration test ตัวใดตัวหนึ่งได้โดยระบุชื่อฟังก์ชัน เทสเป็นอาร์กิวเมนต์ให้ cargo test ในการรันเทสทั้งหมดในไฟล์ integration test ตัวใดตัวหนึ่ง ใช้อาร์กิวเมนต์ --test ของ cargo test ตามด้วยชื่อของไฟล์:

$ cargo test --test integration_test
   Compiling adder v0.1.0 (file:///projects/adder)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.64s
     Running tests/integration_test.rs (target/debug/deps/integration_test-82e7799c1bc62298)

running 1 test
test it_adds_two ... ok

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

คำสั่งนี้รันเฉพาะเทสในไฟล์ tests/integration_test.rs

Submodule ใน Integration Test

เมื่อคุณเพิ่ม integration test มากขึ้น คุณอาจต้องการสร้างไฟล์เพิ่มใน directory tests เพื่อช่วยจัดระเบียบพวกมัน ตัวอย่างเช่น คุณจัดกลุ่ม ฟังก์ชันเทสตาม functionality ที่พวกมันทดสอบได้ ตามที่กล่าวก่อนหน้า แต่ละไฟล์ใน directory tests ถูกคอมไพล์เป็น crate แยกของตัวเอง ซึ่งมี ประโยชน์สำหรับสร้าง scope แยกเพื่อเลียนแบบวิธีที่ end user จะใช้ crate ของคุณใกล้เคียงขึ้น อย่างไรก็ตาม นี่หมายความว่าไฟล์ใน directory tests ไม่แชร์พฤติกรรมเดียวกับไฟล์ใน src ที่คุณเรียนในบทที่ 7 เกี่ยวกับวิธีแยกโค้ดเป็นโมดูลและไฟล์

พฤติกรรมที่ต่างของไฟล์ directory tests เห็นชัดที่สุดเมื่อคุณมีชุด ฟังก์ชัน helper ที่จะใช้ในไฟล์ integration test หลายไฟล์ และคุณ พยายามทำตามขั้นตอนในส่วน “แยก Module ไปคนละไฟล์” ของบทที่ 7 เพื่อดึงพวกมันเป็นโมดูลทั่วไป ตัวอย่างเช่น ถ้าเราสร้าง tests/common.rs และวางฟังก์ชันชื่อ setup ในนั้น เราเพิ่มโค้ดให้ setup ที่เราต้องการเรียกจากฟังก์ชันเทสหลายตัวในไฟล์เทสหลายไฟล์ได้:

Filename: tests/common.rs

pub fn setup() {
    // setup code specific to your library's tests would go here
}

เมื่อเรารันเทสอีกครั้ง เราจะเห็นส่วนใหม่ใน output เทสสำหรับไฟล์ common.rs แม้ว่าไฟล์นี้จะไม่มีฟังก์ชันเทสใด ๆ และเราไม่ได้เรียก ฟังก์ชัน setup จากที่ใด:

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

running 1 test
test tests::internal ... ok

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

     Running tests/common.rs (target/debug/deps/common-92948b65e88960b4)

running 0 tests

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

     Running tests/integration_test.rs (target/debug/deps/integration_test-92948b65e88960b4)

running 1 test
test it_adds_two ... ok

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

   Doc-tests adder

running 0 tests

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

การที่ common ปรากฏในผลเทสพร้อม running 0 tests แสดงสำหรับมัน ไม่ใช่สิ่งที่เราต้องการ เราแค่ต้องการแชร์โค้ดบางส่วนกับไฟล์ integration test อื่น เพื่อหลีกเลี่ยงให้ common ปรากฏใน output เทส แทนที่จะสร้าง tests/common.rs เราจะสร้าง tests/common/mod.rs directory โปรเจกต์ตอนนี้ดูแบบนี้:

├── Cargo.lock
├── Cargo.toml
├── src
│   └── lib.rs
└── tests
    ├── common
    │   └── mod.rs
    └── integration_test.rs

นี่คือธรรมเนียมการตั้งชื่อเก่ากว่าที่ Rust ก็เข้าใจ ที่เรากล่าวถึงใน “Path ไฟล์ทางเลือก” ในบทที่ 7 การตั้งชื่อ ไฟล์แบบนี้บอก Rust ไม่ให้ปฏิบัติกับโมดูล common เป็นไฟล์ integration test เมื่อเราย้ายโค้ดฟังก์ชัน setup ไปยัง tests/common/mod.rs และลบไฟล์ tests/common.rs ส่วนใน output เทส จะไม่ปรากฏอีก ไฟล์ใน subdirectory ของ directory tests ไม่ได้ถูก คอมไพล์เป็น crate แยก หรือมีส่วนใน output เทส

หลังจากเราสร้าง tests/common/mod.rs เราใช้มันจากไฟล์ integration test ใด ๆ ในฐานะโมดูลได้ นี่คือตัวอย่างการเรียกฟังก์ชัน setup จาก เทส it_adds_two ใน tests/integration_test.rs:

Filename: tests/integration_test.rs

use adder::add_two;

mod common;

#[test]
fn it_adds_two() {
    common::setup();

    let result = add_two(2);
    assert_eq!(result, 4);
}

สังเกตว่าการประกาศ mod common; เหมือนกับการประกาศโมดูลที่เราสาธิตใน Listing 7-21 จากนั้นในฟังก์ชันเทส เราเรียกฟังก์ชัน common::setup() ได้

Integration Test สำหรับ Binary Crate

ถ้าโปรเจกต์ของเราเป็น binary crate ที่มีเฉพาะไฟล์ src/main.rs และ ไม่มีไฟล์ src/lib.rs เราสร้าง integration test ใน directory tests และนำฟังก์ชันที่นิยามในไฟล์ src/main.rs เข้า scope ด้วย statement use ไม่ได้ เฉพาะ library crate ที่ expose ฟังก์ชันที่ crate อื่นใช้ ได้ — binary crate มีไว้สำหรับรันด้วยตัวเอง

นี่คือหนึ่งในเหตุผลที่โปรเจกต์ Rust ที่ให้ binary มีไฟล์ src/main.rs ตรงไปตรงมาที่เรียก logic ที่อยู่ในไฟล์ src/lib.rs โดยใช้โครงสร้าง นั้น integration test ทำได้ ทดสอบ library crate ด้วย use เพื่อ ทำให้ functionality ที่สำคัญใช้ได้ ถ้า functionality ที่สำคัญทำงาน โค้ดจำนวนเล็กน้อยในไฟล์ src/main.rs ก็จะทำงานด้วย และโค้ดจำนวน เล็กน้อยนั้นไม่ต้องถูกทดสอบ

สรุป

ฟีเจอร์การเทสของ Rust ให้วิธีระบุว่าโค้ดควรทำงานยังไง เพื่อทำให้แน่ใจ ว่ามันยังทำงานตามที่คุณคาดหวัง แม้เมื่อคุณเปลี่ยน unit test ใช้งานส่วน ต่าง ๆ ของ library แยกกัน และทดสอบรายละเอียด implementation private ได้ integration test ตรวจสอบว่าหลายส่วนของ library ทำงานร่วมกันถูก ต้อง และพวกมันใช้ public API ของ library เพื่อทดสอบโค้ดในแบบเดียวกับ ที่โค้ดภายนอกจะใช้ แม้ว่าระบบ type และกฎ ownership ของ Rust ช่วย ป้องกัน bug บางประเภท เทสยังสำคัญเพื่อลด bug เชิง logic ที่เกี่ยวกับ ว่าโค้ดของคุณคาดหวังให้ทำงานยังไง

มารวมความรู้ที่คุณเรียนในบทนี้และในบทก่อนหน้าเพื่อทำงานในโปรเจกต์กัน!