第六章:测试驱动开发与安全特性
教学目标
- 掌握 Rust 测试框架的使用方法,能够编写单元测试与集成测试
- 深入理解 Rust 所有权与借用检查机制,掌握常见安全问题的诊断与修复
- 理解不可变性与并发安全的关系,掌握基本的并发编程安全实践
- 能够运用测试驱动开发方法确保代码质量,编写符合 Rust 安全规范的程序
核心知识点
1. 测试框架
#[test] 属性与单元测试
Rust 内置测试框架通过#[test]属性标记测试函数,测试失败时会报告错误,成功时无输出。单元测试通常针对单个函数或模块进行。
// 被测试的函数:计算两个数的和
fn add(a: i32, b: i32) -> i32 {
a + b
}
// 单元测试函数,使用#[test]标记
#[test]
fn test_add() {
// 测试正常情况
assert_eq!(add(2, 3), 5);
// 测试边界情况
assert_eq!(add(0, 0), 0);
// 测试负数情况
assert_eq!(add(-5, 10), 5);
}
// 测试失败的案例
#[test]
fn test_add_failure() {
// 这个测试会失败,因为预期结果是5,实际是6
assert_eq!(add(2, 3), 6);
}
测试断言(assert!/assert_eq!)
Rust 提供了多种断言宏,常用的有:
- assert!(condition):条件为 false 时测试失败
- assert_eq!(left, right):左右两边不相等时测试失败
- assert_ne!(left, right):左右两边相等时测试失败
#[test]
fn test_assert_macros() {
let x = 5;
// 断言x大于0
assert!(x > 0, "x should be greater than 0");
let a = 10;
let b = 10;
// 断言a等于b
assert_eq!(a, b, "a and b should be equal");
let c = 5;
let d = 10;
// 断言c不等于d
assert_ne!(c, d, "c and d should not be equal");
}
集成测试与测试覆盖率
集成测试用于测试多个模块之间的交互,通常放在tests目录下,独立于源代码。
// src/lib.rs
pub fn greet(name: &str) -> String {
format!("Hello, {}!", name)
}
// tests/integration_test.rs
use super::greet;
#[test]
fn test_greet() {
let result = greet("Rust");
assert_eq!(result, "Hello, Rust!");
}
// 运行测试
// cargo test --test integration_test
2. 所有权与借用
所有权三大规则
- 每个值都有一个唯一的所有者:当所有者离开作用域时,值会被丢弃
- 值在同一时间只能有一个可变引用或多个不可变引用:防止数据竞争
- 引用必须始终有效:不能存在悬垂引用
fn main() {
// 规则1:变量a是String的所有者,离开作用域时被丢弃
{
let a = String::from("hello");
println!("a: {}", a);
}
// println!("a: {}", a); // 错误:a已离开作用域被丢弃
// 规则2:不可变引用
let b = String::from("world");
let c = &b; // 不可变引用
let d = &b; // 多个不可变引用是允许的
println!("b: {}, c: {}, d: {}", b, c, d);
// 规则2:可变引用
let mut e = String::from("mutable");
let f = &mut e; // 可变引用
*f += " test";
println!("e: {}", e);
// let g = &mut e; // 错误:同一时间只能有一个可变引用
}
借用检查器与错误诊断
Rust 编译器的借用检查器会在编译时检查所有权规则,以下是常见的错误类型:
fn main() {
// 错误1:移动语义 - 变量被移动后无法再次使用
let x = String::from("moved");
let y = x; // x的所有权移动到y
// println!("x: {}", x); // 错误:x已被移动
// 错误2:可变引用与不可变引用冲突
let mut z = String::from("conflict");
let a = &z; // 不可变引用
// let b = &mut z; // 错误:存在不可变引用时无法创建可变引用
// 错误3:悬垂引用 - 引用指向已释放的内存
let reference_to_nothing = {
let a = String::from("dangling");
&a // 返回对a的引用,但a即将离开作用域
};
// println!("{}", reference_to_nothing); // 错误:引用指向已释放的内存
}
生命周期标注
生命周期标注用于告知编译器引用的存活时间,确保引用在使用时有效。
// 没有生命周期标注的函数,会导致编译错误
fn first_word(s: &str) -> &str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[0..]
}
// 带有生命周期标注的函数
fn first_word<'a>(s: &'a str) -> &'a str {
let bytes = s.as_bytes();
for (i, &item) in bytes.iter().enumerate() {
if item == b' ' {
return &s[0..i];
}
}
&s[0..]
}
fn main() {
let s = String::from("hello world");
let word = first_word(&s);
println!("第一个单词: {}", word);
}
3. 不可变性与并发性
不可变性带来的安全保证
不可变数据在多线程环境中是线程安全的,因为没有可变状态需要同步。
use std::thread;
fn main() {
// 不可变数据可以安全地在多个线程间共享
let message = String::from("不可变数据");
// 创建10个线程,每个线程都引用message
for i in 0..10 {
let thread_message = &message;
thread::spawn(move || {
println!("线程 {}: {}", i, thread_message);
});
}
// 等待所有线程完成
thread::sleep(std::time::Duration::from_millis(100));
}
移动语义与克隆 trait
移动语义确保每个值只有一个所有者,Clone trait 允许显式复制数据。
fn main() {
// 移动语义示例
let a = String::from("moved");
let b = a; // a的所有权移动到b,a不再可用
// println!("a: {}", a); // 错误:a已被移动
// Clone trait示例 - 实现了Clone的类型可以复制
let c = String::from("cloned");
let d = c.clone(); // 复制c的值
println!("c: {}, d: {}", c, d); // 输出:c: cloned, d: cloned
// Copy trait示例 - 基本类型默认实现Copy
let e = 10;
let f = e; // 复制e的值,e仍然可用
println!("e: {}, f: {}", e, f); // 输出:e: 10, f: 10
}
引用计数与内部可变性
Rc用于共享不可变数据,RefCell用于在不可变容器中实现可变数据。
use std::rc::Rc;
use std::cell::RefCell;
fn main() {
// Rc<T> - 引用计数,允许多个所有者共享不可变数据
let shared_data = Rc::new(String::from("shared"));
let rc1 = Rc::clone(&shared_data);
let rc2 = Rc::clone(&shared_data);
println!("引用计数: {}", Rc::strong_count(&shared_data)); // 输出:3
// RefCell<T> - 内部可变性,在不可变容器中实现可变数据
let mut cell = RefCell::new(5);
{
let mut value = cell.borrow_mut();
*value = 10;
}
println!("RefCell值: {}", *cell.borrow()); // 输出:10
}
项目实战:为 RustTask 添加测试与安全验证
1. 为 Task 模块添加单元测试
在src/task.rs中添加#[cfg(test)]模块,编写针对Task结构体的单元测试。
// src/task.rs
use std::string::String;
use std::time::SystemTime;
#[derive(Debug, Clone)]
pub struct Task {
pub id: u32,
pub title: String,
pub description: String,
pub due_date: SystemTime,
pub status: TaskStatus,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum TaskStatus {
Todo,
InProgress,
Completed,
}
impl Task {
pub fn new(id: u32, title: String, description: String, due_date: SystemTime) -> Self {
Task {
id,
title,
description,
due_date,
status: TaskStatus::Todo,
}
}
pub fn update_status(&mut self, status: TaskStatus) {
self.status = status;
}
}
// 单元测试模块
#[cfg(test)]
mod tests {
use super::*;
use std::time::{SystemTime, UNIX_EPOCH};
#[test]
fn test_task_creation() {
// 创建任务
let due_date = UNIX_EPOCH + std::time::Duration::from_secs(86400);
let task = Task::new(
1,
"学习Rust".to_string(),
"完成测试章节".to_string(),
due_date,
);
// 断言任务属性正确
assert_eq!(task.id, 1);
assert_eq!(task.title, "学习Rust");
assert_eq!(task.description, "完成测试章节");
assert_eq!(task.status, TaskStatus::Todo);
}
#[test]
fn test_task_status_update() {
// 创建任务
let due_date = UNIX_EPOCH + std::time::Duration::from_secs(86400);
let mut task = Task::new(
1,
"学习Rust".to_string(),
"完成测试章节".to_string(),
due_date,
);
// 更新任务状态
task.update_status(TaskStatus::InProgress);
assert_eq!(task.status, TaskStatus::InProgress);
task.update_status(TaskStatus::Completed);
assert_eq!(task.status, TaskStatus::Completed);
}
}
2. 为 Storage 模块添加集成测试
在src/storage.rs中添加测试,验证文件存储和内存存储的功能。
// src/storage.rs
use crate::task::{Task, TaskStatus};
use std::collections::HashMap;
use std::path::PathBuf;
use serde::{Deserialize, Serialize};
use thiserror::Error;
pub trait TaskStorage {
type Error;
fn save(&self, tasks: &HashMap<u32, Task>, next_id: u32) -> Result<(), Self::Error>;
fn load(&self) -> Result<(HashMap<u32, Task>, u32), Self::Error>;
}
#[derive(Error, Debug)]
pub enum StorageError {
#[error("文件操作错误: {0}")]
FileError(#[from] std::io::Error),
#[error("JSON解析错误: {0}")]
JsonError(#[from] serde_json::Error),
#[error("任务数据格式错误: {0}")]
DataFormatError(String),
}
#[derive(Serialize, Deserialize, Debug)]
struct TaskStorageData {
tasks: HashMap<u32, Task>,
next_id: u32,
}
pub struct FileStorage {
path: PathBuf,
}
impl TaskStorage for FileStorage {
type Error = StorageError;
fn save(&self, tasks: &HashMap<u32, Task>, next_id: u32) -> Result<(), Self::Error> {
let storage = TaskStorageData {
tasks: tasks.clone(),
next_id,
};
let data = serde_json::to_vec(&storage)?;
let mut file = std::fs::File::create(&self.path)?;
file.write_all(&data)?;
Ok(())
}
fn load(&self) -> Result<(HashMap<u32, Task>, u32), Self::Error> {
if !self.path.exists() {
return Ok((HashMap::new(), 1));
}
let data = std::fs::read_to_string(&self.path)?;
let storage: TaskStorageData = serde_json::from_str(&data)?;
Ok((storage.tasks, storage.next_id))
}
}
pub struct MemoryStorage {
tasks: Option<(HashMap<u32, Task>, u32)>,
}
impl TaskStorage for MemoryStorage {
type Error = &'static str;
fn save(&self, tasks: &HashMap<u32, Task>, next_id: u32) -> Result<(), Self::Error> {
self.tasks = Some((tasks.clone(), next_id));
Ok(())
}
fn load(&self) -> Result<(HashMap<u32, Task>, u32), Self::Error> {
match &self.tasks {
Some((tasks, next_id)) => Ok((tasks.clone(), *next_id)),
None => Ok((HashMap::new(), 1)),
}
}
}
// 集成测试
#[cfg(test)]
mod tests {
use super::*;
use crate::task::{Task, TaskStatus};
use std::collections::HashMap;
use std::time::SystemTime;
#[test]
fn test_file_storage() {
// 创建临时文件
let temp_file = tempfile::tempfile().unwrap();
let path = temp_file.path().to_path_buf();
drop(temp_file); // 关闭文件,以便后续创建
// 使用文件存储
let storage = FileStorage::new(path.clone());
// 准备测试数据
let mut tasks = HashMap::new();
let due_date = SystemTime::now();
let task1 = Task::new(1, "任务1".to_string(), "描述1".to_string(), due_date);
let task2 = Task::new(2, "任务2".to_string(), "描述2".to_string(), due_date);
tasks.insert(1, task1);
tasks.insert(2, task2);
// 保存数据
storage.save(&tasks, 3).unwrap();
// 加载数据
let (loaded_tasks, next_id) = storage.load().unwrap();
// 断言数据正确加载
assert_eq!(loaded_tasks.len(), 2);
assert_eq!(next_id, 3);
if let (Some(task1), Some(task2)) = (loaded_tasks.get(&1), loaded_tasks.get(&2)) {
assert_eq!(task1.title, "任务1");
assert_eq!(task2.title, "任务2");
}
}
#[test]
fn test_memory_storage() {
// 使用内存存储
let storage = MemoryStorage { tasks: None };
// 准备测试数据
let mut tasks = HashMap::new();
let due_date = SystemTime::now();
let task1 = Task::new(1, "任务1".to_string(), "描述1".to_string(), due_date);
tasks.insert(1, task1);
// 保存数据
storage.save(&tasks, 2).unwrap();
// 加载数据
let (loaded_tasks, next_id) = storage.load().unwrap();
// 断言数据正确加载
assert_eq!(loaded_tasks.len(), 1);
assert_eq!(next_id, 2);
if let Some(task1) = loaded_tasks.get(&1) {
assert_eq!(task1.title, "任务1");
}
}
}
3. 引入所有权错误并修复
在src/cli.rs中故意引入所有权错误,通过编译器诊断信息理解 Rust 安全特性。
// src/cli.rs
use crate::storage::TaskStorage;
use crate::task::{Task, TaskStatus};
use std::collections::HashMap;
use std::time::SystemTime;
pub struct TaskManager<S> {
tasks: HashMap<u32, Task>,
next_id: u32,
storage: S,
}
impl<S: TaskStorage> TaskManager<S> {
pub fn new(storage: S) -> Result<Self, S::Error> {
let (tasks, next_id) = storage.load()?;
Ok(TaskManager { tasks, next_id, storage })
}
pub fn add_task(&mut self, title: String, description: String, due_date: SystemTime) {
let task = Task::new(self.next_id, title, description, due_date);
self.tasks.insert(self.next_id, task);
self.next_id += 1;
}
// 故意引入所有权错误的函数
pub fn get_task_reference(&self, task_id: u32) -> &Task {
// 错误:返回了对HashMap中值的引用,但HashMap可能被修改
// 实际上,HashMap的引用在修改时会失效,但此处仅作为示例
self.tasks.get(&task_id).unwrap()
}
pub fn update_task_status(&mut self, task_id: u32, status: TaskStatus) {
if let Some(task) = self.tasks.get_mut(&task_id) {
task.update_status(status);
}
}
pub fn save(&self) -> Result<(), S::Error> {
self.storage.save(&self.tasks, self.next_id)
}
}
// 所有权错误示例
fn ownership_error_demo() {
let mut manager = TaskManager::new(MemoryStorage { tasks: None }).unwrap();
manager.add_task(
"错误演示".to_string(),
"所有权错误示例".to_string(),
SystemTime::now(),
);
// 获取任务引用
let task_ref = manager.get_task_reference(1);
println!("任务标题: {}", task_ref.title);
// 错误:同时拥有不可变引用和可变引用,违反所有权规则
// manager.update_task_status(1, TaskStatus::Completed); // 编译错误:cannot borrow `manager` as mutable because it is also borrowed as immutable
}
4. 修复所有权错误并验证
修改代码以修复所有权错误,确保符合 Rust 所有权规则。
// src/cli.rs
// ... 其他代码 ...
impl<S: TaskStorage> TaskManager<S> {
// ... 其他方法 ...
// 修复后的方法:避免同时持有不可变和可变引用
pub fn update_task_status_safely(&mut self, task_id: u32, status: TaskStatus) {
if let Some(task) = self.tasks.get_mut(&task_id) {
task.update_status(status);
}
}
}
// 修复后的所有权示例
fn fixed_ownership_demo() {
let mut manager = TaskManager::new(MemoryStorage { tasks: None }).unwrap();
manager.add_task(
"修复演示".to_string(),
"所有权修复示例".to_string(),
SystemTime::now(),
);
// 正确做法:先更新状态,再获取引用
manager.update_task_status_safely(1, TaskStatus::Completed);
let task_ref = manager.get_task_reference(1);
println!("任务状态: {:?}", task_ref.status); // 应该输出Completed
}
5. 运行测试并验证安全特性
使用cargo test命令运行测试,确保所有测试通过,并观察所有权错误的编译提示。
# 运行所有测试
cargo test
# 输出示例
# running 4 tests
# test task::tests::test_task_creation ... ok
# test task::tests::test_task_status_update ... ok
# test storage::tests::test_file_storage ... ok
# test storage::tests::test_memory_storage ... ok
#
# test result: ok. 4 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out
实践作业
为TaskManager添加集成测试,覆盖添加、更新、删除任务的完整流程,并修复一个所有权相关的错误,具体要求:
- 在src/cli.rs中添加remove_task方法,用于删除任务
- 在tests目录下创建集成测试文件cli_integration_test.rs
- 编写测试用例,覆盖以下场景:
-
- 添加任务
-
- 更新任务状态
-
- 删除任务
-
- 验证数据持久化
- 故意引入一个所有权错误(如同时持有可变和不可变引用)
- 修复所有权错误,确保代码通过编译和测试
// src/cli.rs
impl<S: TaskStorage> TaskManager<S> {
// 添加删除任务的方法
pub fn remove_task(&mut self, task_id: u32) -> Result<(), &'static str> {
if self.tasks.remove(&task_id).is_none() {
Err("任务不存在")
} else {
Ok(())
}
}
}
// tests/cli_integration_test.rs
use crate::cli::TaskManager;
use crate::storage::MemoryStorage;
use crate::task::{Task, TaskStatus};
use std::time::SystemTime;
#[test]
fn test_task_manager_workflow() {
let storage = MemoryStorage { tasks: None };
let mut manager = TaskManager::new(storage).unwrap();
// 添加任务
manager.add_task(
"测试任务".to_string(),
"集成测试任务".to_string(),
SystemTime::now(),
);
// 更新任务状态
manager.update_task_status(1, TaskStatus::Completed);
// 删除任务
manager.remove_task(1).unwrap();
// 验证任务已删除
assert!(manager.tasks.is_empty());
}
// 故意引入的所有权错误
#[test]
fn test_ownership_error() {
let storage = MemoryStorage { tasks: None };
let mut manager = TaskManager::new(storage).unwrap();
manager.add_task(
"错误任务".to_string(),
"所有权错误测试".to_string(),
SystemTime::now(),
);
// 错误:同时获取不可变引用和可变引用
let task_ref = manager.get_task_reference(1);
manager.remove_task(1).unwrap(); // 编译错误:cannot borrow `manager` as mutable because it is also borrowed as immutable
println!("任务已删除: {}", task_ref.title); // 悬垂引用
}
通过完成这个作业,你将深入理解 Rust 的测试框架和所有权机制,学会如何编写可靠的测试用例,并确保代码符合 Rust 的安全规范,避免常见的内存安全问题。