在前面的篇章中,我們理解了如何用工具診斷問題。
今天我們要探討更根本的問題:如何從一開始就確保程式碼的正確性?
關鍵洞察:測試不是為了覆蓋率,而是為了建立信心。
// 測試就是一個標記了 #[test] 的函式
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
// 執行測試
// cargo test
#[test]
fn test_failure() {
assert_eq!(2 + 2, 5); // 測試會失敗
}
// 輸出:
// assertion failed: `(left == right)`
// left: `4`,
// right: `5`
#[test]
#[should_panic]
fn test_panic() {
panic!("這個測試預期會 panic");
}
#[test]
#[should_panic(expected = "除數不能為零")]
fn test_divide_by_zero() {
divide(10, 0); // 應該 panic 並包含特定訊息
}
fn divide(a: i32, b: i32) -> i32 {
if b == 0 {
panic!("除數不能為零");
}
a / b
}
// src/lib.rs
fn internal_function(x: i32) -> i32 {
x * 2
}
pub fn public_function(x: i32) -> i32 {
internal_function(x) + 1
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_internal() {
// 可以測試私有函式
assert_eq!(internal_function(5), 10);
}
#[test]
fn test_public() {
assert_eq!(public_function(5), 11);
}
}
fn parse_number(s: &str) -> Result<i32, String> {
s.parse().map_err(|_| format!("無法解析 '{}'", s))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_valid_number() {
assert_eq!(parse_number("42").unwrap(), 42);
}
#[test]
fn test_invalid_number() {
assert!(parse_number("abc").is_err());
}
#[test]
fn test_error_message() {
let err = parse_number("xyz").unwrap_err();
assert_eq!(err, "無法解析 'xyz'");
}
}
fn safe_divide(a: i32, b: i32) -> Option<i32> {
if b == 0 {
None
} else {
Some(a / b)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_normal_division() {
assert_eq!(safe_divide(10, 2), Some(5));
}
#[test]
fn test_divide_by_zero() {
assert_eq!(safe_divide(10, 0), None);
}
#[test]
fn test_negative_numbers() {
assert_eq!(safe_divide(-10, 2), Some(-5));
assert_eq!(safe_divide(10, -2), Some(-5));
}
#[test]
fn test_overflow() {
// i32::MIN / -1 會溢位
assert_eq!(safe_divide(i32::MIN, -1), Some(i32::MIN / -1));
}
}
my_project/
├── src/
│ └── lib.rs
└── tests/
├── integration_test.rs
└── common/
└── mod.rs
// tests/integration_test.rs
use my_project::*;
#[test]
fn test_public_api() {
let result = public_function(5);
assert_eq!(result, 11);
}
#[test]
fn test_workflow() {
// 測試完整的工作流程
let config = Config::new("test.conf").unwrap();
let processor = Processor::new(config);
let result = processor.process("input");
assert_eq!(result, "expected output");
}
// tests/common/mod.rs
pub fn setup() -> TestEnvironment {
TestEnvironment {
temp_dir: create_temp_dir(),
config: load_test_config(),
}
}
pub struct TestEnvironment {
pub temp_dir: PathBuf,
pub config: Config,
}
impl Drop for TestEnvironment {
fn drop(&mut self) {
// 清理測試環境
std::fs::remove_dir_all(&self.temp_dir).ok();
}
}
// tests/integration_test.rs
mod common;
#[test]
fn test_with_setup() {
let env = common::setup();
// 使用測試環境
}
不是測試具體的輸入輸出,而是測試函式應該滿足的性質。
use proptest::prelude::*;
// 性質:反轉兩次應該得到原始值
proptest! {
#[test]
fn test_reverse_twice(s in ".*") {
let reversed = reverse(&s);
let double_reversed = reverse(&reversed);
prop_assert_eq!(s, double_reversed);
}
}
fn reverse(s: &str) -> String {
s.chars().rev().collect()
}
use proptest::prelude::*;
fn my_sort(vec: &mut Vec<i32>) {
vec.sort();
}
proptest! {
#[test]
fn test_sort_properties(mut vec in prop::collection::vec(any::<i32>(), 0..100)) {
let original_len = vec.len();
my_sort(&mut vec);
// 性質 1:長度不變
prop_assert_eq!(vec.len(), original_len);
// 性質 2:已排序
for i in 1..vec.len() {
prop_assert!(vec[i - 1] <= vec[i]);
}
// 性質 3:包含所有原始元素(可以用 HashMap 計數驗證)
}
}
use proptest::prelude::*;
struct BankAccount {
balance: i64,
}
impl BankAccount {
fn new(initial: i64) -> Result<Self, String> {
if initial < 0 {
return Err("初始餘額不能為負".to_string());
}
Ok(BankAccount { balance: initial })
}
fn deposit(&mut self, amount: i64) -> Result<(), String> {
if amount <= 0 {
return Err("存款金額必須為正".to_string());
}
self.balance = self.balance.checked_add(amount)
.ok_or("餘額溢位".to_string())?;
Ok(())
}
fn withdraw(&mut self, amount: i64) -> Result<(), String> {
if amount <= 0 {
return Err("提款金額必須為正".to_string());
}
if amount > self.balance {
return Err("餘額不足".to_string());
}
self.balance -= amount;
Ok(())
}
}
proptest! {
#[test]
fn test_account_invariants(
initial in 0i64..1000,
deposits in prop::collection::vec(1i64..100, 0..10),
withdrawals in prop::collection::vec(1i64..50, 0..10),
) {
let mut account = BankAccount::new(initial).unwrap();
// 執行一系列操作
for amount in deposits {
let _ = account.deposit(amount);
}
for amount in withdrawals {
let _ = account.withdraw(amount);
}
// 不變式:餘額永遠不會是負數
prop_assert!(account.balance >= 0);
}
}
use tokio;
#[tokio::test]
async fn test_async_function() {
let result = async_function().await;
assert_eq!(result, "expected");
}
async fn async_function() -> String {
tokio::time::sleep(tokio::time::Duration::from_millis(100)).await;
"expected".to_string()
}
use tokio::time::{timeout, Duration};
#[tokio::test]
async fn test_with_timeout() {
let result = timeout(
Duration::from_secs(1),
slow_function()
).await;
assert!(result.is_ok(), "函式執行超時");
}
async fn slow_function() -> String {
tokio::time::sleep(Duration::from_millis(100)).await;
"done".to_string()
}
use tokio::sync::Mutex;
use std::sync::Arc;
#[tokio::test]
async fn test_concurrent_access() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter = Arc::clone(&counter);
let handle = tokio::spawn(async move {
let mut num = counter.lock().await;
*num += 1;
});
handles.push(handle);
}
for handle in handles {
handle.await.unwrap();
}
let final_count = *counter.lock().await;
assert_eq!(final_count, 10);
}
#[cfg(test)]
mod tests {
use super::*;
mod parsing {
use super::*;
#[test]
fn test_parse_valid() { }
#[test]
fn test_parse_invalid() { }
}
mod validation {
use super::*;
#[test]
fn test_validate_email() { }
#[test]
fn test_validate_phone() { }
}
}
struct TestFixture {
temp_dir: PathBuf,
db: Database,
}
impl TestFixture {
fn new() -> Self {
let temp_dir = create_temp_dir();
let db = Database::connect(&temp_dir).unwrap();
TestFixture { temp_dir, db }
}
}
impl Drop for TestFixture {
fn drop(&mut self) {
std::fs::remove_dir_all(&self.temp_dir).ok();
}
}
#[test]
fn test_with_fixture() {
let fixture = TestFixture::new();
// 使用 fixture.db
}
#[test]
fn test_multiple_cases() {
let test_cases = vec![
("input1", "output1"),
("input2", "output2"),
("input3", "output3"),
];
for (input, expected) in test_cases {
let result = process(input);
assert_eq!(result, expected, "失敗於輸入: {}", input);
}
}
# 安裝
cargo install cargo-tarpaulin
# 執行覆蓋率分析
cargo tarpaulin --out Html
# 會生成 tarpaulin-report.html
// 100% 覆蓋率不等於正確
fn buggy_function(x: i32) -> i32 {
if x > 0 {
x + 1 // 應該是 x * 2
} else {
x - 1
}
}
#[test]
fn test_buggy() {
// 這個測試覆蓋了所有分支
assert_eq!(buggy_function(5), 6); // 通過(但邏輯錯誤)
assert_eq!(buggy_function(-5), -6); // 通過
}
關鍵洞察:覆蓋率是必要條件,不是充分條件。重要的是測試正確的行為。
// 1. 紅:先寫測試(會失敗)
#[test]
fn test_add() {
assert_eq!(add(2, 3), 5);
}
// 2. 綠:寫最簡單的實作(通過測試)
fn add(a: i32, b: i32) -> i32 {
a + b
}
// 3. 重構:改進程式碼品質
fn add(a: i32, b: i32) -> i32 {
a.checked_add(b).expect("溢位")
}
// 4. 添加更多測試
#[test]
fn test_add_overflow() {
// 測試邊界情況
}
// 1. 先寫測試
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_new_stack_is_empty() {
let stack: Stack<i32> = Stack::new();
assert!(stack.is_empty());
}
#[test]
fn test_push_and_pop() {
let mut stack = Stack::new();
stack.push(1);
stack.push(2);
assert_eq!(stack.pop(), Some(2));
assert_eq!(stack.pop(), Some(1));
assert_eq!(stack.pop(), None);
}
}
// 2. 實作
pub struct Stack<T> {
items: Vec<T>,
}
impl<T> Stack<T> {
pub fn new() -> Self {
Stack { items: Vec::new() }
}
pub fn push(&mut self, item: T) {
self.items.push(item);
}
pub fn pop(&mut self) -> Option<T> {
self.items.pop()
}
pub fn is_empty(&self) -> bool {
self.items.is_empty()
}
}
#[test]
fn test_small_unit() {
// 測試單一函式
// 執行快速
// 失敗時容易定位
}
#[test]
fn test_integration() {
// 測試多個模組協作
// 模擬真實使用
// 確保介面正確
}
proptest! {
#[test]
fn test_property(input in strategy) {
// 測試不變式
// 自動生成測試案例
// 發現意外的邊界情況
}
}
#[tokio::test]
async fn test_async() {
// 測試非同步邏輯
// 驗證併發安全
// 檢查超時行為
}
關鍵洞察:好的測試策略是多層次的。單元測試提供快速反饋,整合測試確保協作正確,性質測試發現邊界問題,非同步測試驗證併發安全。
在最後一篇中,我們將總結整個系列,看看如何將 Rust 的思維應用到其他語言。