如何测试Rust fallible函数未覆盖代码?fail-rs是否可行?
如何测试Rust函数中行读取错误的分支?fail-rs是否适用?
一、测试未覆盖的行读取错误分支
要测试那部分未覆盖的代码,核心是构造读取行时会返回IO错误的场景。最可靠的方式是通过重构函数抽象读取逻辑,便于注入测试用的错误数据源,具体步骤如下:
1. 重构函数,分离文件打开与读取逻辑
将原函数拆分为两个部分:一个负责打开文件,另一个接受实现Read trait的对象处理读取逻辑,这样测试时可以传入自定义的错误数据源:
use std::io::{self, BufRead, BufReader, Read}; use anyhow::Result; use log::{error, trace}; pub fn fallible(source_file: &str) -> Result<bool> { trace!("Opening file {}", source_file); let file = std::fs::File::open(source_file).map_err(|e| { error!("{e}"); anyhow::Error::msg(format!("Failed to open {source_file}")) })?; fallible_reader(file) } // 新增的核心逻辑函数,接受任意Read实现 pub fn fallible_reader<R: Read>(reader: R) -> Result<bool> { let reader = BufReader::new(reader); let mut matched = false; let line_fallback = String::from(""); for (_idx, line) in reader.lines().enumerate() { let line = line.as_ref().unwrap_or_else(|e| { error!("{e}"); &line_fallback }); if line.to_lowercase().contains("query") { matched = true; break; } } if !matched { trace!("Found no query reference"); } Ok(matched) }
2. 实现自定义Read类型模拟行读取错误
创建一个自定义的Read实现,在特定时机返回IO错误,用来触发目标分支:
#[cfg(test)] mod tests { use super::*; use std::io::{self, Read}; use env_logger; // 自定义Read类型:第一次读取正常,第二次读取返回错误 struct ErrOnSecondRead { count: usize, } impl Read for ErrOnSecondRead { fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> { self.count += 1; if self.count == 2 { Err(io::Error::new(io::ErrorKind::Other, "test line read error")) } else { let data = b"sample line\n"; let len = data.len().min(buf.len()); buf[..len].copy_from_slice(&data[..len]); Ok(len) } } } #[test] fn handles_line_read_error() { // 初始化日志捕获,用于验证error!宏是否被调用 let _ = env_logger::builder().is_test(true).try_init(); let reader = ErrOnSecondRead { count: 0 }; let result = fallible_reader(reader); // 验证函数正常返回,且结果为false(错误行被替换为空,不包含"query") assert_eq!(result.unwrap(), false); // 可选:使用`logtest`等库捕获日志,验证错误信息是否正确输出 // let logs = logtest::Logger::start(); // // ...执行测试逻辑 // assert!(logs.any(|record| record.level() == log::Level::Error && record.args().to_string().contains("test line read error"))); } // 保留原有测试用例 #[test] fn true_if_reference_is_found() { let ret = fallible("mocks/reference.txt"); assert_eq!(ret.unwrap(), true); } #[test] fn false_if_reference_is_not_found() { let ret = fallible("mocks/no-reference.txt"); assert_eq!(ret.unwrap(), false); } #[test] fn error_if_file_does_not_exist() { let ret = fallible("./not-existant-file.txt"); assert_eq!(ret.is_err(), true); } }
这种方式不依赖外部库,测试逻辑清晰可控,是最推荐的方案。
二、fail-rs是否为可行的测试方案?
fail-rs是一个用于故障注入的Rust crate,可以通过hook系统调用来模拟底层错误,它确实可以用来测试这个场景,但需要注意以下几点:
1. 可行性说明
fail-rs可以在文件读取的系统调用(如read)中注入错误,从而触发reader.lines()返回IO错误,命中目标分支。示例代码如下:
#[cfg(test)] mod tests { use super::*; use tempfile::NamedTempFile; use std::io::Write; use fail; #[test] fn test_line_read_error_with_fail() { let _ = env_logger::builder().is_test(true).try_init(); // 创建临时文件并写入内容 let mut temp_file = NamedTempFile::new().unwrap(); writeln!(temp_file, "first line").unwrap(); writeln!(temp_file, "second line").unwrap(); temp_file.flush().unwrap(); let path = temp_file.path().to_str().unwrap(); // 注入故障:当调用File::read时,第二次调用返回错误 let fail_point = fail::FailPoint::new("std::fs::File::read", fail::FailScenario::Return(Err(std::io::Error::new(std::io::ErrorKind::Other, "test error")))); fail_point.set_probability(1.0); let result = fallible(path); assert_eq!(result.unwrap(), false); } }
2. 优缺点
- 优点:无需重构原有函数,直接模拟底层系统级错误,适合测试真实环境下的故障场景。
- 缺点:
- 需要启用
unsafe代码,依赖系统调用hook,不同平台(如Windows/Linux/macOS)的行为可能存在差异; - 故障注入的粒度较难精确控制,可能误触发其他系统调用的错误(比如文件打开阶段的
read操作); - 增加了测试的复杂性,需要额外管理故障点的生命周期。
- 需要启用
总结
- 优先选择重构抽象读取逻辑的方式测试,代码侵入性低,测试可靠且易于维护;
- fail-rs是可行的方案,但更适合模拟底层系统故障,需要权衡其复杂性和平台兼容性后使用。
内容的提问来源于stack exchange,提问作者Luca Anceschi
相关产品推荐
相关产品推荐

