Keywords: testing, tooling, error handling, macros
อ่านแบบคน Python:
- ถ้าอยากเอา “ภาพรวม” ก่อน: test คือกันชนเวลาคุณ refactor (ไม่ใช่ภาระ)
- ถ้าอยาก “ลงมือทำ”: แตก logic ให้เป็นฟังก์ชันที่ pure-ish แล้วเขียน unit test ก่อนประกอบกับ I/O
- ถ้าติด: เปิด 12-learning-playbook.md
บทนี้สอน test/debug แบบ practical สำหรับคนมาจาก Python: โครงสร้าง test ใน Rust, การอ่าน assertion error, การใช้ dbg!, และ workflow ที่เข้ากับ cargo test/clippy
แก่นของบทนี้:
- แยก logic ออกจาก I/O แล้ว test เฉพาะส่วนที่ pure-ish
- เขียน test ให้จับ “พฤติกรรม” (ไม่ใช่จับแค่บรรทัดโค้ด)
- ใช้
dbg!/--nocaptureเพื่อ debug ให้เร็ว แล้วค่อยลบ/เก็บให้สะอาด
Python (เทียบแนวคิด):
pytestมักรันไฟล์test_*.py- หรือ
unittestแบบ class-based
Rust มี 2 แบบหลัก:
- Unit tests
- อยู่ไฟล์เดียวกับโค้ด
- อยู่ใน
mod testsและเปิดด้วย#[cfg(test)]
- Integration tests
- อยู่ในโฟลเดอร์
tests/ - เป็น crate แยก (เรียก public API ของ lib)
ตัวอย่าง unit test:
fn add(a: i32, b: i32) -> i32 {
a + b
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_works() {
assert_eq!(add(2, 3), 5);
}
}Output (example):
(no output — when tests pass, they usually produce no stdout)
พื้นฐาน:
assert!(cond)assert_eq!(left, right)assert_ne!(left, right)
เคล็ดลับ:
- เวลา
assert_eq!fail Rust มักแสดง diff ให้ → ช่วยไล่ bug เร็ว
หลักคิดแบบ Python:
assert_eq!เป็น “บรรทัดล็อกความหมาย” ของฟังก์ชัน มากกว่าเป็นแค่การเช็ก
#[test]
#[should_panic]
fn it_panics() {
panic!("boom");
}Output (example):
(no output — this test passes because it panics as expected)
ข้อควรระวัง:
- ถ้าเป็นโค้ดระดับแอป/ไลบรารีทั่วไป มักอยากให้คืน
Resultมากกว่า panic - ใช้
should_panicได้ในเคสที่คุณกำลังทดสอบ guard rails หรือ invariants ภายใน
Rust มี dbg! ที่พิมพ์ค่า + file:line แล้วคืนค่าเดิม
let x = dbg!(2 + 3);Output (example):
[src/main.rs:LINE] 2 + 3 = 5
แนวคิด: LINE คือเลขบรรทัดจริงในเครื่องคุณ (dbg! พิมพ์ file:line อัตโนมัติ)
เหมาะมากเวลาอยู่ใน iterator chain แล้วอยากดูค่าระหว่างทาง:
let out: Vec<i32> = nums
.iter()
.map(|n| dbg!(n * 2))
.collect();Output (example):
[src/main.rs:LINE] n * 2 = 2
[src/main.rs:LINE] n * 2 = 4
[src/main.rs:LINE] n * 2 = 6
โดย default ตอน test ผ่าน output อาจถูกซ่อนไว้
ถ้าต้องการเห็น output:
cargo test -- --nocapture
คนมาจาก Python มักเริ่มจาก wiring ทั้งโปรแกรมแล้วค่อย debug
ใน Rust จะง่ายกว่า ถ้าคุณ:
- แตก logic เป็น function ที่รับ input → คืน output (ไม่มี I/O)
- เขียน unit test ครอบก่อน
- แล้วค่อยประกอบเข้ากับ I/O
ตัวอย่างแนวคิด (โยงบท 09/11):
- ทำ
fn parse_config_str(text: &str) -> Result<Config, Error> - test ด้วย string literal ไม่ต้องอ่านไฟล์
คำสั่งที่ใช้บ่อย:
cargo testcargo test name_of_test(รันเฉพาะบางอัน)cargo test -- --nocapture(เห็น stdout)
เวลาต้องไล่ bug ให้เร็ว:
- สร้าง test ที่ fail แบบ reproducible
- แก้โค้ดจน test ผ่าน
- ค่อย refactor (แล้วให้ test คุม)
println!("{:?}", v)ต้องให้ type implementDebugprintln!("{}", err)ต้องมีDisplay
ระหว่างพัฒนา:
- ใส่
#[derive(Debug)]ช่วยได้มาก
แนวคิดสำคัญ:
- error message ที่ดีช่วยลดเวลาทั้งของ dev และ user
- เวลาใช้
Resultให้ใส่ context ให้ชัดว่า “พังตรงไหน” (เช่น file path)
- เขียน
fn normalize(s: &str) -> Stringที่ trim + lowercase
- เขียน unit tests อย่างน้อย 3 เคส (มีช่องว่าง/มีตัวพิมพ์ใหญ่/empty)
- เขียน
fn parse_ints(xs: &[&str]) -> Result<Vec<i32>, std::num::ParseIntError>
- เขียน test ทั้งเคสสำเร็จ และเคส error
-
ทำ iterator chain แล้วแทรก
dbg!เพื่อดูค่าระหว่างmap/filter -
(ท้าทาย) ทำ integration test แบบง่าย ๆ (ถ้าคุณมี Rust crate จริงใน repo นี้)
- ถ้า repo นี้เป็นเอกสารอย่างเดียว ให้ข้ามข้อ 4