You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何测试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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.13 07:00:55