嗨嗨!大家好!歡迎來到 Rust 三十天挑戰的第十四天!
經過前十三天的深入學習,我們已經掌握了 Rust 的核心概念,從基礎語法到智慧指標,從所有權系統到泛型與特徵。今天我們要來學習一個在實際開發中極其重要的主題:測試 (Testing)。
如果說前面學的是如何「寫程式」,那麼今天要學的就是如何「確保程式正確」。在現代軟體開發中,測試不只是「可有可無」的加分項,而是「必備」的核心技能。Rust 內建了強大的測試框架,讓我們能夠輕鬆地為程式碼建立完整的測試體系。
老實說,剛開始寫測試時,我覺得這是在「浪費時間」—為什麼要寫這麼多看似重複的程式碼?但隨著專案規模的增長,我深深體會到測試的價值:它們是我重構程式碼時的安全感,是新功能開發時的信心來源,避免改A壞B,更是程式碼品質的守護者。
今天讓我們一起探索 Rust 的測試世界!
在深入語法之前,讓我們先理解為什麼測試如此重要:
1. 及早發現錯誤:在開發階段就抓到 bug,而不是等到使用者發現
2. 重構的安全感:當你需要改進程式碼時,測試能確保功能沒有被破壞
3. 程式碼文件:良好的測試本身就是程式碼的使用說明書
4. 設計改善:編寫測試會迫使你思考 API 的設計是否合理
5. 團隊協作:其他開發者可以透過測試了解程式碼的預期行為
Rust 的測試框架有以下特色:
在 Rust 中,測試函式使用 #[test]
屬性標記:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn it_works() {
let result = 2 + 2;
assert_eq!(result, 4);
}
#[test]
fn test_basic_assertions() {
// assert! - 檢查布林值
assert!(true);
assert!(2 + 2 == 4);
// assert_eq! - 檢查相等性
assert_eq!(2 + 2, 4);
assert_eq!("hello", "hello");
// assert_ne! - 檢查不相等性
assert_ne!(2 + 2, 5);
assert_ne!("hello", "world");
}
}
執行測試:
cargo test
讓我們建立一個實際的例子來展示測試的各種用法:
/// 計算矩形面積
pub struct Rectangle {
width: f64,
height: f64,
}
impl Rectangle {
pub fn new(width: f64, height: f64) -> Result<Self, String> {
if width <= 0.0 || height <= 0.0 {
Err("寬度和高度必須大於 0".to_string())
} else {
Ok(Rectangle { width, height })
}
}
pub fn area(&self) -> f64 {
self.width * self.height
}
pub fn perimeter(&self) -> f64 {
2.0 * (self.width + self.height)
}
pub fn can_hold(&self, other: &Rectangle) -> bool {
self.width >= other.width && self.height >= other.height
}
pub fn is_square(&self) -> bool {
(self.width - self.height).abs() < f64::EPSILON
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rectangle_creation() {
let rect = Rectangle::new(10.0, 20.0);
assert!(rect.is_ok());
let rect = rect.unwrap();
assert_eq!(rect.width, 10.0);
assert_eq!(rect.height, 20.0);
}
#[test]
fn test_rectangle_invalid_dimensions() {
let result = Rectangle::new(-5.0, 10.0);
assert!(result.is_err());
let result = Rectangle::new(5.0, 0.0);
assert!(result.is_err());
}
#[test]
fn test_area_calculation() {
let rect = Rectangle::new(3.0, 4.0).unwrap();
assert_eq!(rect.area(), 12.0);
let square = Rectangle::new(5.0, 5.0).unwrap();
assert_eq!(square.area(), 25.0);
}
#[test]
fn test_perimeter_calculation() {
let rect = Rectangle::new(3.0, 4.0).unwrap();
assert_eq!(rect.perimeter(), 14.0);
}
#[test]
fn test_can_hold() {
let larger = Rectangle::new(8.0, 7.0).unwrap();
let smaller = Rectangle::new(5.0, 1.0).unwrap();
let same_size = Rectangle::new(8.0, 7.0).unwrap();
assert!(larger.can_hold(&smaller));
assert!(!smaller.can_hold(&larger));
assert!(larger.can_hold(&same_size));
}
#[test]
fn test_is_square() {
let square = Rectangle::new(5.0, 5.0).unwrap();
let rectangle = Rectangle::new(5.0, 3.0).unwrap();
assert!(square.is_square());
assert!(!rectangle.is_square());
}
#[test]
fn test_floating_point_precision() {
// 處理浮點數精度問題
let rect = Rectangle::new(0.1, 0.2).unwrap();
let expected_area = 0.02;
// 使用 epsilon 比較浮點數
assert!((rect.area() - expected_area).abs() < f64::EPSILON);
}
}
有時候我們需要測試程式碼在特定條件下會 panic:
pub fn divide(a: f64, b: f64) -> f64 {
if b == 0.0 {
panic!("不能除以零!");
}
a / b
}
pub fn parse_positive_number(s: &str) -> Result<u32, String> {
match s.parse::<u32>() {
Ok(num) if num > 0 => Ok(num),
Ok(_) => Err("數字必須大於 0".to_string()),
Err(_) => Err("無法解析為數字".to_string()),
}
}
#[cfg(test)]
mod math_tests {
use super::*;
#[test]
fn test_divide_normal() {
assert_eq!(divide(10.0, 2.0), 5.0);
assert_eq!(divide(7.0, 3.0), 7.0 / 3.0);
}
#[test]
#[should_panic]
fn test_divide_by_zero() {
divide(10.0, 0.0);
}
#[test]
#[should_panic(expected = "不能除以零")]
fn test_divide_by_zero_with_message() {
divide(10.0, 0.0);
}
#[test]
fn test_parse_positive_number_success() {
assert_eq!(parse_positive_number("42"), Ok(42));
assert_eq!(parse_positive_number("1"), Ok(1));
}
#[test]
fn test_parse_positive_number_zero() {
assert_eq!(
parse_positive_number("0"),
Err("數字必須大於 0".to_string())
);
}
#[test]
fn test_parse_positive_number_negative() {
// 注意:parse::<u32> 本身就會拒絕負數
assert!(parse_positive_number("-5").is_err());
}
#[test]
fn test_parse_positive_number_invalid() {
assert_eq!(
parse_positive_number("abc"),
Err("無法解析為數字".to_string())
);
assert_eq!(
parse_positive_number(""),
Err("無法解析為數字".to_string())
);
}
}
有時候某些測試需要很長時間執行,或者需要特殊的環境設定:
#[cfg(test)]
mod expensive_tests {
use std::thread;
use std::time::Duration;
#[test]
fn quick_test() {
assert_eq!(2 + 2, 4);
}
#[test]
#[ignore]
fn expensive_test() {
// 模擬耗時操作
thread::sleep(Duration::from_secs(1));
assert_eq!(expensive_computation(), 42);
}
fn expensive_computation() -> i32 {
// 假設這是一個很耗時的計算
thread::sleep(Duration::from_millis(100));
42
}
}
執行被忽略的測試:
cargo test -- --ignored
執行所有測試(包括被忽略的):
cargo test -- --include-ignored
# 只運行名稱包含 "rectangle" 的測試
cargo test rectangle
# 運行特定的測試函式
cargo test test_area_calculation
# 運行特定模組的測試
cargo test math_tests
# 顯示測試中的 println! 輸出
cargo test -- --nocapture
# 單執行緒運行測試(避免並行問題)
cargo test -- --test-threads=1
整合測試用於測試函式庫的公開 API,放在 tests/
目錄中:
// 檔案結構
// src/
// lib.rs
// tests/
// integration_test.rs
// common/
// mod.rs
//! 一個簡單的部落格文章管理函式庫
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub struct Post {
pub id: u32,
pub title: String,
pub content: String,
pub author: String,
pub published: bool,
}
impl Post {
pub fn new(id: u32, title: String, content: String, author: String) -> Self {
Post {
id,
title,
content,
author,
published: false,
}
}
pub fn publish(&mut self) {
self.published = true;
}
pub fn unpublish(&mut self) {
self.published = false;
}
pub fn update_content(&mut self, new_content: String) {
self.content = new_content;
}
}
pub struct Blog {
posts: HashMap<u32, Post>,
next_id: u32,
}
impl Blog {
pub fn new() -> Self {
Blog {
posts: HashMap::new(),
next_id: 1,
}
}
pub fn create_post(&mut self, title: String, content: String, author: String) -> u32 {
let id = self.next_id;
self.next_id += 1;
let post = Post::new(id, title, content, author);
self.posts.insert(id, post);
id
}
pub fn get_post(&self, id: u32) -> Option<&Post> {
self.posts.get(&id)
}
pub fn get_post_mut(&mut self, id: u32) -> Option<&mut Post> {
self.posts.get_mut(&id)
}
pub fn delete_post(&mut self, id: u32) -> bool {
self.posts.remove(&id).is_some()
}
pub fn published_posts(&self) -> Vec<&Post> {
self.posts
.values()
.filter(|post| post.published)
.collect()
}
pub fn posts_by_author(&self, author: &str) -> Vec<&Post> {
self.posts
.values()
.filter(|post| post.author == author)
.collect()
}
pub fn post_count(&self) -> usize {
self.posts.len()
}
}
impl Default for Blog {
fn default() -> Self {
Self::new()
}
}
// 單元測試仍然在 src 檔案中
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_post_creation() {
let post = Post::new(
1,
"測試標題".to_string(),
"測試內容".to_string(),
"測試作者".to_string(),
);
assert_eq!(post.id, 1);
assert_eq!(post.title, "測試標題");
assert_eq!(post.content, "測試內容");
assert_eq!(post.author, "測試作者");
assert!(!post.published);
}
#[test]
fn test_post_publish() {
let mut post = Post::new(
1,
"標題".to_string(),
"內容".to_string(),
"作者".to_string(),
);
assert!(!post.published);
post.publish();
assert!(post.published);
post.unpublish();
assert!(!post.published);
}
}
// 整合測試檔案
use rust_blog::{Blog, Post};
#[test]
fn test_blog_workflow() {
let mut blog = Blog::new();
// 測試建立文章
let post_id = blog.create_post(
"我的第一篇文章".to_string(),
"這是我的第一篇部落格文章!".to_string(),
"Alice".to_string(),
);
assert_eq!(post_id, 1);
assert_eq!(blog.post_count(), 1);
// 測試取得文章
let post = blog.get_post(post_id).unwrap();
assert_eq!(post.title, "我的第一篇文章");
assert!(!post.published);
// 測試發布文章
blog.get_post_mut(post_id).unwrap().publish();
let published_posts = blog.published_posts();
assert_eq!(published_posts.len(), 1);
// 測試刪除文章
assert!(blog.delete_post(post_id));
assert_eq!(blog.post_count(), 0);
assert!(!blog.delete_post(post_id)); // 重複刪除應該失敗
}
#[test]
fn test_multiple_authors() {
let mut blog = Blog::new();
// Alice 寫了兩篇文章
let alice_post1 = blog.create_post(
"Alice 的第一篇".to_string(),
"內容1".to_string(),
"Alice".to_string(),
);
let alice_post2 = blog.create_post(
"Alice 的第二篇".to_string(),
"內容2".to_string(),
"Alice".to_string(),
);
// Bob 寫了一篇文章
let bob_post = blog.create_post(
"Bob 的文章".to_string(),
"Bob 的內容".to_string(),
"Bob".to_string(),
);
// 發布 Alice 的第一篇和 Bob 的文章
blog.get_post_mut(alice_post1).unwrap().publish();
blog.get_post_mut(bob_post).unwrap().publish();
// 測試按作者查詢
let alice_posts = blog.posts_by_author("Alice");
assert_eq!(alice_posts.len(), 2);
let bob_posts = blog.posts_by_author("Bob");
assert_eq!(bob_posts.len(), 1);
// 測試已發布文章
let published = blog.published_posts();
assert_eq!(published.len(), 2);
}
#[test]
fn test_post_content_update() {
let mut blog = Blog::new();
let post_id = blog.create_post(
"可編輯的文章".to_string(),
"原始內容".to_string(),
"Editor".to_string(),
);
// 更新內容
blog.get_post_mut(post_id)
.unwrap()
.update_content("更新後的內容".to_string());
let post = blog.get_post(post_id).unwrap();
assert_eq!(post.content, "更新後的內容");
assert_eq!(post.title, "可編輯的文章"); // 標題不變
}
共用的測試工具函式:
// 共用的測試輔助函式
use rust_blog::{Blog, Post};
pub fn create_sample_blog() -> Blog {
let mut blog = Blog::new();
let post1_id = blog.create_post(
"Rust 入門".to_string(),
"Rust 是一個系統程式語言...".to_string(),
"Rust 愛好者".to_string(),
);
let post2_id = blog.create_post(
"測試的重要性".to_string(),
"測試可以確保程式碼品質...".to_string(),
"測試專家".to_string(),
);
let post3_id = blog.create_post(
"進階 Rust 技巧".to_string(),
"智慧指標和生命週期...".to_string(),
"Rust 愛好者".to_string(),
);
// 發布前兩篇文章
blog.get_post_mut(post1_id).unwrap().publish();
blog.get_post_mut(post2_id).unwrap().publish();
blog
}
pub fn assert_post_equals(actual: &Post, expected: &Post) {
assert_eq!(actual.id, expected.id);
assert_eq!(actual.title, expected.title);
assert_eq!(actual.content, expected.content);
assert_eq!(actual.author, expected.author);
assert_eq!(actual.published, expected.published);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_sample_blog_creation() {
let blog = create_sample_blog();
assert_eq!(blog.post_count(), 3);
assert_eq!(blog.published_posts().len(), 2);
}
}
使用共用工具的進階整合測試:
use rust_blog::Blog;
mod common;
#[test]
fn test_blog_statistics() {
let blog = common::create_sample_blog();
// 測試總體統計
assert_eq!(blog.post_count(), 3);
assert_eq!(blog.published_posts().len(), 2);
// 測試作者統計
let rust_enthusiast_posts = blog.posts_by_author("Rust 愛好者");
assert_eq!(rust_enthusiast_posts.len(), 2);
let test_expert_posts = blog.posts_by_author("測試專家");
assert_eq!(test_expert_posts.len(), 1);
// 測試不存在的作者
let nonexistent_posts = blog.posts_by_author("不存在的作者");
assert_eq!(nonexistent_posts.len(), 0);
}
#[test]
fn test_published_content_only() {
let blog = common::create_sample_blog();
let published_posts = blog.published_posts();
// 確保只有已發布的文章被回傳
for post in published_posts {
assert!(post.published);
}
// 檢查特定的已發布文章
let titles: Vec<&String> = published_posts.iter().map(|p| &p.title).collect();
assert!(titles.contains(&&"Rust 入門".to_string()));
assert!(titles.contains(&&"測試的重要性".to_string()));
assert!(!titles.contains(&&"進階 Rust 技巧".to_string()));
}
Rust 可以執行文件註解中的程式碼範例,確保文件和實際程式碼保持同步:
/// 計算兩個數字的最大公約數
///
/// 使用歐幾里得演算法來計算兩個正整數的最大公約數。
///
/// # 參數
///
/// * `a` - 第一個正整數
/// * `b` - 第二個正整數
///
/// # 回傳值
///
/// 回傳 `a` 和 `b` 的最大公約數
///
/// # 範例
///
/// ```
/// use rust_blog::gcd;
///
/// let result = gcd(48, 18);
/// assert_eq!(result, 6);
///
/// let result = gcd(17, 13);
/// assert_eq!(result, 1);
/// ```
///
/// # Panics
///
/// 當任一參數為 0 時會 panic:
///
/// ```should_panic
/// use rust_blog::gcd;
///
/// gcd(0, 5); // 這會 panic
/// ```
pub fn gcd(mut a: u32, mut b: u32) -> u32 {
assert!(a != 0 && b != 0, "GCD 的參數不能為 0");
while b != 0 {
let temp = b;
b = a % b;
a = temp;
}
a
}
/// 檢查一個數字是否為質數
///
/// # 範例
///
/// ```
/// use rust_blog::is_prime;
///
/// assert!(is_prime(2));
/// assert!(is_prime(17));
/// assert!(!is_prime(4));
/// assert!(!is_prime(1));
/// ```
///
/// 對於大數字的檢查:
///
/// ```
/// use rust_blog::is_prime;
///
/// // 這些是已知的質數
/// assert!(is_prime(97));
/// assert!(is_prime(101));
///
/// // 這些不是質數
/// assert!(!is_prime(99));
/// assert!(!is_prime(100));
/// ```
pub fn is_prime(n: u32) -> bool {
if n < 2 {
return false;
}
if n == 2 {
return true;
}
if n % 2 == 0 {
return false;
}
let limit = (n as f64).sqrt() as u32 + 1;
for i in (3..limit).step_by(2) {
if n % i == 0 {
return false;
}
}
true
}
執行文件測試:
cargo test --doc
對於需要外部依賴的程式碼,我們可以使用特徵來建立可測試的抽象:
use std::collections::HashMap;
// 定義資料庫介面
pub trait Database {
fn get_user(&self, id: u32) -> Option<String>;
fn save_user(&mut self, id: u32, name: String) -> Result<(), String>;
fn delete_user(&mut self, id: u32) -> bool;
}
// 真實的資料庫實現(簡化版)
pub struct SqlDatabase {
connection_string: String,
}
impl SqlDatabase {
pub fn new(connection_string: String) -> Self {
SqlDatabase { connection_string }
}
}
impl Database for SqlDatabase {
fn get_user(&self, _id: u32) -> Option<String> {
// 實際會連接資料庫查詢
None
}
fn save_user(&mut self, _id: u32, _name: String) -> Result<(), String> {
// 實際會寫入資料庫
Ok(())
}
fn delete_user(&mut self, _id: u32) -> bool {
// 實際會從資料庫刪除
true
}
}
// 使用者服務
pub struct UserService<D: Database> {
db: D,
}
impl<D: Database> UserService<D> {
pub fn new(db: D) -> Self {
UserService { db }
}
pub fn get_user_info(&self, id: u32) -> String {
match self.db.get_user(id) {
Some(name) => format!("用戶 {}: {}", id, name),
None => format!("找不到用戶 {}", id),
}
}
pub fn create_user(&mut self, id: u32, name: String) -> Result<String, String> {
if name.is_empty() {
return Err("用戶名稱不能為空".to_string());
}
self.db.save_user(id, name.clone())?;
Ok(format!("成功建立用戶:{}", name))
}
pub fn remove_user(&mut self, id: u32) -> String {
if self.db.delete_user(id) {
format!("用戶 {} 已被刪除", id)
} else {
format!("刪除用戶 {} 失敗", id)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
// 測試用的模擬資料庫
struct MockDatabase {
users: HashMap<u32, String>,
should_fail_save: bool,
}
impl MockDatabase {
fn new() -> Self {
MockDatabase {
users: HashMap::new(),
should_fail_save: false,
}
}
fn with_users(users: Vec<(u32, String)>) -> Self {
let mut db = MockDatabase::new();
for (id, name) in users {
db.users.insert(id, name);
}
db
}
fn set_save_failure(&mut self, should_fail: bool) {
self.should_fail_save = should_fail;
}
}
impl Database for MockDatabase {
fn get_user(&self, id: u32) -> Option<String> {
self.users.get(&id).cloned()
}
fn save_user(&mut self, id: u32, name: String) -> Result<(), String> {
if self.should_fail_save {
Err("資料庫錯誤".to_string())
} else {
self.users.insert(id, name);
Ok(())
}
}
fn delete_user(&mut self, id: u32) -> bool {
self.users.remove(&id).is_some()
}
}
#[test]
fn test_get_existing_user() {
let db = MockDatabase::with_users(vec![(1, "Alice".to_string())]);
let service = UserService::new(db);
let result = service.get_user_info(1);
assert_eq!(result, "用戶 1: Alice");
}
#[test]
fn test_get_nonexistent_user() {
let db = MockDatabase::new();
let service = UserService::new(db);
let result = service.get_user_info(999);
assert_eq!(result, "找不到用戶 999");
}
#[test]
fn test_create_user_success() {
let db = MockDatabase::new();
let mut service = UserService::new(db);
let result = service.create_user(1, "Bob".to_string());
assert_eq!(result, Ok("成功建立用戶:Bob".to_string()));
// 驗證用戶確實被保存
let user_info = service.get_user_info(1);
assert_eq!(user_info, "用戶 1: Bob");
}
#[test]
fn test_create_user_empty_name() {
let db = MockDatabase::new();
let mut service = UserService::new(db);
let result = service.create_user(1, "".to_string());
assert_eq!(result, Err("用戶名稱不能為空".to_string()));
}
#[test]
fn test_create_user_database_failure() {
let mut db = MockDatabase::new();
db.set_save_failure(true);
let mut service = UserService::new(db);
let result = service.create_user(1, "Charlie".to_string());
assert_eq!(result, Err("資料庫錯誤".to_string()));
}
#[test]
fn test_remove_existing_user() {
let db = MockDatabase::with_users(vec![(1, "David".to_string())]);
let mut service = UserService::new(db);
let result = service.remove_user(1);
assert_eq!(result, "用戶 1 已被刪除");
// 驗證用戶確實被刪除
let user_info = service.get_user_info(1);
assert_eq!(user_info, "找不到用戶 1");
}
#[test]
fn test_remove_nonexistent_user() {
let db = MockDatabase::new();
let mut service = UserService::new(db);
let result = service.remove_user(999);
assert_eq!(result, "刪除用戶 999 失敗");
}
}
對於重複的測試模式,我們可以建立自訂的測試巨集:
// 建立一個巨集來簡化數學函式的測試
macro_rules! test_math_function {
($func_name:ident, $test_name:ident, $input:expr, $expected:expr) => {
#[test]
fn $test_name() {
let result = $func_name($input.0, $input.1);
assert_eq!(result, $expected);
}
};
// 支援多組測試資料
($func_name:ident, $test_name:ident, [$(($input:expr, $expected:expr)),+ $(,)?]) => {
#[test]
fn $test_name() {
$(
let result = $func_name($input.0, $input.1);
assert_eq!(result, $expected,
"Failed for input {:?}, expected {}, got {}",
$input, $expected, result);
)+
}
};
}
// 自訂斷言巨集
macro_rules! assert_approximately_eq {
($left:expr, $right:expr, $epsilon:expr) => {
assert!(
($left - $right).abs() < $epsilon,
"assertion failed: `(left ~= right)`\n left: `{}`,\n right: `{}`,\n epsilon: `{}`",
$left,
$right,
$epsilon
);
};
($left:expr, $right:expr) => {
assert_approximately_eq!($left, $right, f64::EPSILON);
};
}
// 使用自訂巨集進行測試
fn add(a: i32, b: i32) -> i32 {
a + b
}
fn multiply(a: i32, b: i32) -> i32 {
a * b
}
fn divide(a: f64, b: f64) -> f64 {
a / b
}
test_math_function!(add, test_add_positive, (2, 3), 5);
test_math_function!(add, test_add_negative, (-2, -3), -5);
test_math_function!(multiply, test_multiply_basic, (4, 5), 20);
test_math_function!(add, test_add_multiple, [
((1, 1), 2),
((5, 7), 12),
((-3, 8), 5),
((0, 0), 0),
]);
#[cfg(test)]
mod float_tests {
use super::*;
#[test]
fn test_division_with_custom_assertion() {
assert_approximately_eq!(divide(22.0, 7.0), 3.142857, 0.000001);
assert_approximately_eq!(divide(1.0, 3.0), 0.333333, 0.000001);
}
#[test]
fn test_floating_point_arithmetic() {
let result = 0.1 + 0.2;
// 不能直接用 == 比較浮點數
// assert_eq!(result, 0.3); // 這會失敗!
// 使用自訂的近似相等斷言
assert_approximately_eq!(result, 0.3, 1e-10);
}
}
#[cfg(test)]
mod concurrent_tests {
use std::sync::{Arc, Mutex};
use std::thread;
use std::time::Duration;
// 模擬共享資源
static mut GLOBAL_COUNTER: i32 = 0;
static MUTEX_COUNTER: Mutex<i32> = Mutex::new(0);
#[test]
fn test_thread_safe_counter() {
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let counter_clone = Arc::clone(&counter);
let handle = thread::spawn(move || {
for _ in 0..100 {
let mut num = counter_clone.lock().unwrap();
*num += 1;
}
});
handles.push(handle);
}
for handle in handles {
handle.join().unwrap();
}
assert_eq!(*counter.lock().unwrap(), 1000);
}
#[test]
#[ignore] // 這個測試需要單獨執行,因為使用了全域變數
fn test_global_state() {
unsafe {
GLOBAL_COUNTER = 0;
GLOBAL_COUNTER += 1;
assert_eq!(GLOBAL_COUNTER, 1);
}
}
#[test]
fn test_timing_sensitive_operation() {
let start = std::time::Instant::now();
// 模擬一個耗時操作
thread::sleep(Duration::from_millis(100));
let elapsed = start.elapsed();
// 測試操作是否在合理時間內完成
assert!(elapsed >= Duration::from_millis(100));
assert!(elapsed < Duration::from_millis(200));
}
}
今天我們深入探討了 Rust 的測試生態系統:
核心概念:
#[test]
屬性和 assert!
系列巨集tests/
目錄中,測試公開 API實用技巧:
#[cfg(test)]
模組進階主題:
為什麼測試很重要?
測試是現代軟體開發不可或缺的一環。Rust 的測試框架雖然簡單,但功能強大且易於使用。掌握測試技巧將讓你能夠開發出更可靠、更容易維護的軟體。
為了鞏固今天的學習,嘗試為一個圖書館管理系統建立完整的測試套件:
功能需求:
測試要求:
技術提示:
// 定義核心結構
pub struct Book {
isbn: String,
title: String,
author: String,
available: bool,
}
pub struct Member {
id: u32,
name: String,
email: String,
borrowed_books: Vec<String>, // ISBN 列表
}
pub struct Library<D: Database> {
database: D,
}
// 定義資料庫介面以便測試
pub trait Database {
fn save_book(&mut self, book: &Book) -> Result<(), String>;
fn find_book(&self, isbn: &str) -> Option<Book>;
fn save_member(&mut self, member: &Member) -> Result<(), String>;
// 更多方法...
}
// 測試範例
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_book_creation() {
// 測試書籍建立
}
#[test]
fn test_member_borrow_book() {
// 測試借書流程
}
#[test]
#[should_panic(expected = "書籍已被借出")]
fn test_borrow_unavailable_book() {
// 測試借閱不可用書籍的錯誤處理
}
}
這個挑戰將讓你綜合運用今天學到的所有測試技巧:單元測試、整合測試、模擬物件、錯誤測試等。重點是建立一個完整、可靠的測試體系,確保圖書館系統的每個功能都經過充分驗證。
明天我們將開始第三週的學習,探討閉包 (Closures) 與迭代器 (Iterators),這些函數式程式設計的概念將讓我們能夠寫出更簡潔、更高效的程式碼!
如果在實作過程中遇到任何問題,歡迎在留言區討論。測試技能需要在實際專案中不斷練習和改進,但建立良好的測試習慣將讓你受益終生!
我們明天見!