การควบคุมวิธีรันการทดสอบ (Controlling How Tests Are Run)
เช่นเดียวกับที่ cargo run คอมไพล์โค้ดของคุณแล้วรันไบนารีที่ได้ คำสั่ง cargo test จะคอมไพล์โค้ดของคุณในโหมดการทดสอบและรันไบนารีทดสอบที่ได้ พฤติกรรมเริ่มต้นของไบนารีที่สร้างโดย cargo test คือการรันการทดสอบทั้งหมดแบบขนาน (parallel) และดักจับผลลัพธ์ (capture output) ที่เกิดขึ้นระหว่างการรันการทดสอบ ซึ่งจะป้องกันไม่ให้ผลลัพธ์ถูกแสดงออกมาและช่วยให้การอ่านผลการทดสอบง่ายยิ่งขึ้น อย่างไรก็ตาม คุณสามารถระบุตัวเลือกบรรทัดคำสั่ง (command line options) เพื่อเปลี่ยนแปลงพฤติกรรมเริ่มต้นนี้ได้
ตัวเลือกบรรทัดคำสั่งบางตัวส่งไปยัง cargo test และบางตัวส่งไปยังไบนารีทดสอบที่ได้ เพื่อแยกแยะอาร์กิวเมนต์สองประเภทนี้ คุณจะต้องแสดงรายการอาร์กิวเมนต์ที่ส่งไปยัง cargo test ตามด้วยตัวแยก -- จากนั้นตามด้วยอาร์กิวเมนต์ที่จะส่งไปยังไบนารีทดสอบ การรัน cargo test --help จะแสดงตัวเลือกที่คุณสามารถใช้กับ cargo test ได้ และการรัน cargo test -- --help จะแสดงตัวเลือกที่คุณสามารถใช้หลังตัวแยก อ็อปชันเหล่านี้ยังมีอธิบายในเอกสาร ส่วน “Tests” ของหนังสือ The rustc Book
การรันการทดสอบแบบขนานหรือแบบตามลำดับ (Running Tests in Parallel or Consecutively)
เมื่อคุณรันการทดสอบหลายรายการ ตามค่าเริ่มต้น การทดสอบจะถูกรันแบบขนานโดยใช้เธรด (threads) ซึ่งหมายความว่าจะทำงานเสร็จเร็วขึ้นและคุณจะได้รับข้อติชม (feedback) เร็วยิ่งขึ้น เนื่องจากรายการทดสอบกำลังรันพร้อมๆ กัน คุณต้องตรวจสอบให้แน่ใจว่าการทดสอบของคุณไม่ได้พึ่งพากันและกัน หรือพึ่งพาสถานะที่ใช้ร่วมกัน (shared state) รวมถึงสภาพแวดล้อมที่ใช้ร่วมกัน เช่น ไดเรกทอรีทำงานปัจจุบันหรือตัวแปรสภาพแวดล้อม (environment variables)
ตัวอย่างเช่น สมมติว่าการทดสอบแต่ละตัวของคุณรันโค้ดที่สร้างไฟล์บนดิสก์ชื่อ test-output.txt และเขียนข้อมูลลงในไฟล์นั้น จากนั้นการทดสอบแต่ละตัวอ่านข้อมูลในไฟล์นั้นและยืนยันว่าไฟล์นั้นมีค่าตามที่กำหนด ซึ่งแตกต่างกันในการทดสอบแต่ละตัว เนื่องจากรายการทดสอบรันพร้อมกัน การทดสอบหนึ่งอาจเขียนทับไฟล์ในระหว่างเวลาที่อีกการทดสอบหนึ่งกำลังเขียนและอ่านไฟล์ การทดสอบที่สองก็จะล้มเหลว ไม่ใช่เพราะโค้ดไม่ถูกต้อง แต่เป็นเพราะการทดสอบแทรกแซงกันเองขณะรันแบบขนาน ทางแก้หนึ่งคือทำให้แน่ใจว่าแต่ละการทดสอบเขียนลงไฟล์คนละไฟล์กัน ทางแก้อีกทางหนึ่งคือการรันการทดสอบทีละตัว
หากคุณไม่อยากรันการทดสอบแบบขนาน หรือต้องการการควบคุมจำนวนเธรดที่ใช้อย่างละเอียดมากขึ้น คุณสามารถส่งแฟล็ก --test-threads และจำนวนเธรดที่คุณต้องการใช้ไปยังไบนารีทดสอบได้ ลองดูตัวอย่างต่อไปนี้:
$ cargo test -- --test-threads=1
เราตั้งค่าจำนวนเธรดทดสอบเป็น 1 เพื่อบอกโปรแกรมว่าไม่ต้องใช้การทำงานแบบขนาน การรันการทดสอบโดยใช้เธรดเดียวจะใช้เวลานานกว่าการรันแบบขนาน แต่การทดสอบจะไม่แทรกแซงกันและกันหากมีการใช้สถานะร่วมกัน
การแสดงผลลัพธ์ของฟังก์ชัน (Showing Function Output)
ตามค่าเริ่มต้น หากการทดสอบผ่าน ไลบรารีการทดสอบของ Rust จะดักจับ (capture) สิ่งใดก็ตามที่พิมพ์ไปยัง standard output ตัวอย่างเช่น หากเราเรียกใช้ println! ในการทดสอบและการทดสอบนั้นผ่าน เราจะไม่เห็นผลลัพธ์ของ println! ในเทอร์มินัล เราจะเห็นเพียงบรรทัดที่ระบุว่าการทดสอบผ่าน หากการทดสอบล้มเหลว เราจะเห็นสิ่งที่ถูกพิมพ์ไปยัง standard output พร้อมกับส่วนที่เหลือของข้อความความล้มเหลว
ตัวอย่างเช่น Listing 11-10 มีฟังก์ชันสมมติที่พิมพ์ค่าของพารามิเตอร์และคืนค่า 10 รวมถึงการทดสอบที่ผ่านและการทดสอบที่ล้มเหลว
fn prints_and_returns_10(a: i32) -> i32 {
println!("I got the value {a}");
10
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn this_test_will_pass() {
let value = prints_and_returns_10(4);
assert_eq!(value, 10);
}
#[test]
fn this_test_will_fail() {
let value = prints_and_returns_10(8);
assert_eq!(value, 5);
}
}
เมื่อเรารันการทดสอบเหล่านี้ด้วย cargo test เราจะเห็นผลลัพธ์ดังนี้:
$ cargo test
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.58s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' (6019863) panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
สังเกตว่าไม่มีส่วนใดในผลลัพธ์นี้ที่แสดง I got the value 4 ซึ่งถูกพิมพ์เมื่อการทดสอบที่ผ่านรัน ผลลัพธ์นั้นถูกดักจับไว้ ส่วนผลลัพธ์จากการทดสอบที่ล้มเหลว I got the value 8 จะปรากฏในส่วนสรุปผลการทดสอบ ซึ่งแสดงสาเหตุของการล้มเหลวด้วย
หากเราต้องการเห็นค่าที่พิมพ์สำหรับการทดสอบที่ผ่านด้วย เราสามารถบอกให้ Rust แสดงผลลัพธ์ของการทดสอบที่สำเร็จได้ด้วย --show-output:
$ cargo test -- --show-output
เมื่อเรารันการทดสอบใน Listing 11-10 อีกครั้งพร้อมกับแฟล็ก --show-output เราจะเห็นผลลัพธ์ดังต่อไปนี้:
$ cargo test -- --show-output
Compiling silly-function v0.1.0 (file:///projects/silly-function)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/silly_function-160869f38cff9166)
running 2 tests
test tests::this_test_will_fail ... FAILED
test tests::this_test_will_pass ... ok
successes:
---- tests::this_test_will_pass stdout ----
I got the value 4
successes:
tests::this_test_will_pass
failures:
---- tests::this_test_will_fail stdout ----
I got the value 8
thread 'tests::this_test_will_fail' (6022313) panicked at src/lib.rs:19:9:
assertion `left == right` failed
left: 10
right: 5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
failures:
tests::this_test_will_fail
test result: FAILED. 1 passed; 1 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
error: test failed, to rerun pass `--lib`
การรันส่วนย่อยของการทดสอบตามชื่อ (Running a Subset of Tests by Name)
การรันชุดการทดสอบทั้งหมด (full test suite) ในบางครั้งอาจใช้เวลานาน หากคุณกำลังทำงานกับโค้ดในส่วนใดส่วนหนึ่ง คุณอาจต้องการรันเฉพาะการทดสอบที่เกี่ยวข้องกับโค้ดส่วนนั้น คุณสามารถเลือกการทดสอบที่จะรันได้โดยการส่งชื่อการทดสอบที่คุณต้องการรันเป็นอาร์กิวเมนต์ไปยัง cargo test
เพื่อสาธิตวิธีรันส่วนย่อยของการทดสอบ ขั้นแรกเราจะสร้างการทดสอบสามรายการสำหรับฟังก์ชัน add_two ของเรา ดังแสดงใน Listing 11-11 และเลือกตัวที่จะรัน
pub fn add_two(a: u64) -> u64 {
a + 2
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn add_two_and_two() {
let result = add_two(2);
assert_eq!(result, 4);
}
#[test]
fn add_three_and_two() {
let result = add_two(3);
assert_eq!(result, 5);
}
#[test]
fn one_hundred() {
let result = add_two(100);
assert_eq!(result, 102);
}
}
หากเรารันการทดสอบโดยไม่ส่งอาร์กิวเมนต์ใดๆ ดังที่เราเห็นก่อนหน้านี้ การทดสอบทั้งหมดจะรันแบบขนาน:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.62s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 3 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test tests::one_hundred ... ok
test result: ok. 3 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
การรันการทดสอบเดี่ยว (Running Single Tests)
เราสามารถส่งชื่อฟังก์ชันการทดสอบใดๆ ไปยัง cargo test เพื่อรันเฉพาะการทดสอบนั้นได้:
$ cargo test one_hundred
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.69s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::one_hundred ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 2 filtered out; finished in 0.00s
มีเพียงการทดสอบที่มีชื่อ one_hundred เท่านั้นที่รัน การทดสอบอีกสองรายการไม่ตรงกับชื่อนั้น ผลลัพธ์การทดสอบแจ้งให้เราทราบว่ามีรายการทดสอบที่ไม่ได้รันเพิ่มเติมโดยแสดง 2 filtered out ที่ตอนท้าย
เราไม่สามารถระบุชื่อของการทดสอบหลายรายการด้วยวิธีนี้ ค่าแรกที่ส่งไปยัง cargo test เท่านั้นที่จะถูกนำไปใช้ แต่มันมีวิธีในการรันการทดสอบหลายรายการอยู่
การกรองเพื่อรันการทดสอบหลายรายการ (Filtering to Run Multiple Tests)
เราสามารถระบุส่วนหนึ่งของชื่อการทดสอบ และการทดสอบใดๆ ที่มีชื่อตรงกับค่านั้นจะถูกรัน ตัวอย่างเช่น เนื่องจากชื่อการทดสอบสองรายการของเรามีคำว่า add เราสามารถรันสองรายการนั้นได้โดยรัน cargo test add:
$ cargo test add
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::add_three_and_two ... ok
test tests::add_two_and_two ... ok
test result: ok. 2 passed; 0 failed; 0 ignored; 0 measured; 1 filtered out; finished in 0.00s
คำสั่งนี้รันการทดสอบทั้งหมดที่มีคำว่า add อยู่ในชื่อ และกรองการทดสอบชื่อ one_hundred ออก นอกจากนี้ ให้สังเกตว่าโมดูลที่การทดสอบปรากฏอยู่จะกลายเป็นส่วนหนึ่งของชื่อการทดสอบ ดังนั้นเราจึงสามารถรันการทดสอบทั้งหมดในโมดูลได้โดยกรองตามชื่อของโมดูล
การข้ามการทดสอบเว้นแต่จะมีการร้องขอเป็นพิเศษ (Ignoring Tests Unless Specifically Requested)
บางครั้งการทดสอบบางรายการอาจใช้เวลารันนานมาก คุณจึงอาจต้องการยกเว้นรายการเหล่านั้นในระหว่างการรัน cargo test ตามปกติ แทนที่จะระบุรายการทดสอบทั้งหมดที่คุณต้องการรันเป็นอาร์กิวเมนต์ คุณสามารถระบุ attribute ignore ให้กับการทดสอบที่ใช้เวลานานเพื่อยกเว้นพวกมันได้ ดังแสดงที่นี่:
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);
}
#[test]
#[ignore]
fn expensive_test() {
// code that takes an hour to run
}
}
หลัง #[test] เราจะเพิ่มบรรทัด #[ignore] ลงในการทดสอบที่เราต้องการยกเว้น คราวนี้เมื่อเรารันการทดสอบ it_works จะรัน แต่ expensive_test จะไม่รัน:
$ cargo test
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.60s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 2 tests
test tests::expensive_test ... ignored
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 1 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
ฟังก์ชัน expensive_test ถูกระบุว่าเป็น ignored หากเราต้องการรันเฉพาะการทดสอบที่ถูกข้าม เราสามารถใช้ cargo test -- --ignored:
$ cargo test -- --ignored
Compiling adder v0.1.0 (file:///projects/adder)
Finished `test` profile [unoptimized + debuginfo] target(s) in 0.61s
Running unittests src/lib.rs (target/debug/deps/adder-92948b65e88960b4)
running 1 test
test tests::expensive_test ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 1 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
การควบคุมรายการทดสอบที่จะรัน จะทำให้คุณมั่นใจได้ว่าผลลัพธ์ของ cargo test จะถูกส่งกลับมาอย่างรวดเร็ว เมื่อคุณอยู่ในจุดที่สมควรตรวจสอบผลลัพธ์ของ ignored และมีเวลาคอยผลลัพธ์ คุณสามารถรัน cargo test -- --ignored แทนได้ หากคุณต้องการรันการทดสอบทั้งหมด ไม่ว่าจะถูกข้ามหรือไม่ก็ตาม คุณสามารถรัน cargo test -- --include-ignored