第六章:测试驱动开发与安全特性

107 阅读11分钟

第六章:测试驱动开发与安全特性

教学目标

  • 掌握 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. 所有权与借用

所有权三大规则
  1. 每个值都有一个唯一的所有者:当所有者离开作用域时,值会被丢弃
  1. 值在同一时间只能有一个可变引用或多个不可变引用:防止数据竞争
  1. 引用必须始终有效:不能存在悬垂引用
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添加集成测试,覆盖添加、更新、删除任务的完整流程,并修复一个所有权相关的错误,具体要求:

  1. 在src/cli.rs中添加remove_task方法,用于删除任务
  1. 在tests目录下创建集成测试文件cli_integration_test.rs
  1. 编写测试用例,覆盖以下场景:
    • 添加任务
    • 更新任务状态
    • 删除任务
    • 验证数据持久化
  1. 故意引入一个所有权错误(如同时持有可变和不可变引用)
  1. 修复所有权错误,确保代码通过编译和测试
// 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 的安全规范,避免常见的内存安全问题。