是否有更简洁的方法来测试使用需要用户在 Rust 中输入的函数的函数?

dat*_*rom 2 unit-testing rust

我正在为我的第一个 Rust 项目编写一个CLI 问题询问库,因为我可能无论如何都会使用它,而且我找不到一种干净的方法来测试terminal构建器模式的方法,该方法使用配置获取用户输入并返回答案。

pub fn confirm(&mut self) -> Answer {
    self.yes_no();
    self.build_prompt();
    let prompt = self.prompt.clone();
    let valid_responses = self.valid_responses.clone().unwrap();
    loop {
        let stdio = io::stdin();
        let input = stdio.lock();
        let output = io::stdout();
        if let Ok(response) = prompt_user(input, output, &prompt) {
            for key in valid_responses.keys() {
                if *response.trim().to_lowercase() == *key {
                    return valid_responses.get(key).unwrap().clone();
                }
            }
            self.build_clarification();
        }
    }
}
Run Code Online (Sandbox Code Playgroud)

在寻找解决方案时,我发现了依赖注入,它允许我为提示用户使用Cursor. 它不允许我confirm()为每个测试更改用户对函数的输入,Question::new("Continue?").confirm()所以我尝试使用条件编译,并提出以下内容。

#[cfg(not(test))]
fn prompt_user<R, W>(mut reader: R, mut writer: W, question: &str) -> Result<String, std::io::Error>
where
    R: BufRead,
    W: Write,
{
    write!(&mut writer, "{}", question)?;
    let mut s = String::new();
    reader.read_line(&mut s)?;
    Ok(s)
}

#[cfg(test)]
fn prompt_user<R, W>(mut reader: R, mut writer: W, question: &str) -> Result<String, std::io::Error>
where
    R: BufRead,
    W: Write,
{
    use tests;
    Ok(unsafe { tests::test_response.to_string() })
}
Run Code Online (Sandbox Code Playgroud)

tests模块中,我使用了一个全局变量:

pub static mut test_response: &str = "";

#[test]
fn simple_confirm() {
    unsafe { test_response = "y" };
    let answer = Question::new("Continue?").confirm();
    assert_eq!(Answer::YES, answer);
}
Run Code Online (Sandbox Code Playgroud)

只要我只使用单个线程运行测试,这就有效,但也不再允许我测试真实的用户输入功能。对于这么小的板条箱来说并不是问题,但它非常凌乱。我没有从任何可用的测试库中看到任何解决方案来做到这一点。

Mat*_* M. 5

正如您链接堆栈溢出问题中提到的,如果您想要可测试性,通常应该避免硬连接外部依赖项(又名 I/O):

  • 磁盘访问,
  • 终端接入,
  • 网络访问,
  • 数据库访问,
  • 时间访问。

在所有这些情况下,我建议使用依赖注入

  • 创建一个干净的界面(特征)来描述允许的操作(不要过度,YAGNI!),
  • 实现“生产”使用的接口,背后有真正的外部依赖,
  • 实现接口的“模拟”以供“测试”使用。

然后,在编写时:

  • 需要访问此资源的函数,将其作为参数传递,
  • 需要访问此资源的方法,将其作为参数或在对象的构造函数中传递。

最后,在 main 中实例化生产依赖项,并从那里转发它们。


技巧,而不是款待:

  • 创建一个Environment包含所有此类接口的结构可能很有用,而不是将大量参数传递给每个函数;然而,只需要一个/两个资源的函数应该明确地使用这些资源来明确它们的用途,
  • 我发现传递时间戳而不是过去从中获取它的时钟很有用......只是因为now()随着时间的推移多次调用可能会返回不同的结果。

  • @user9993:Rust 没有*依赖注入框架*,但仍然可以手动传递依赖。下一个障碍是通用性和引用,我建议使用 `Rc&lt;RefCell&lt;Trait&gt;&gt;` 或它的多线程对应物来保存特征:无论如何,与 CPU 时间相比,外部依赖关系很慢,额外的内存分配和虚拟调用几乎不会是雷达上的一个昙花一现。 (3认同)