在Rust中,有没有一种更简洁的方法来测试使用需要用户输入的函数的函数?

6
我正在为我的第一个Rust项目编写一个CLI问题询问库,因为我很可能会在其中使用它。我无法找到一种干净的测试终端方法的建造者模式的方法,该方法使用配置获取用户输入并返回答案。
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();
        }
    }
}

在寻找解决方案时,我发现了依赖注入,它使我能够为使用Cursor提示用户输入的函数编写测试。然而,它不允许我为Question::new("Continue?").confirm()的每个测试更改用户输入到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() })
}

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);
}

只要我使用单个线程运行测试,这个方法就可以工作,但是也不再允许我测试真实的用户输入函数。对于这样一个小的箱子来说,这并不是问题,但非常混乱。我没有看到任何可用的测试库中有解决此问题的方案。

它不允许我为每个输入更改函数的输入。您能否澄清一下您的意思? - Shepmaster
因为readerwriterquestion都是在confirm()函数内部传递给prompt_user()的,所以我无法像测试prompt_user()函数本身时那样操纵输入。因此,我无法自动化涉及confirm()的测试。 - datenstrom
1
那么,您是说没有任何参数的 confirm 不支持依赖注入吗?如果是这样,我不确定我们能提供什么帮助。是的,您必须有一种注入依赖项的方式,参数是最简单的方法,但您也可以使用结构字段。是什么阻止您在 confirm 上使用参数来注入所需的依赖项呢? - Shepmaster
我并不希望confirm有任何参数,这只是出于公共接口设计的考虑。但我从未想过在confirm中使用结构字段作为prompt_user的参数。我认为这可能可行,并且可以让我在测试时修改输入源。 - datenstrom
1个回答

10
如你所提到的Stack Overflow问题中所述,如果要实现可测试性,通常应避免硬编码外部依赖项(也称为I/O):
  • 磁盘访问,
  • 终端访问,
  • 网络访问,
  • 数据库访问,
  • 时间访问。
在所有这些情况下,我建议使用依赖注入
  • 创建一个干净的接口(trait)来描述允许的操作(不要过度设计,YAGNI!),
  • 为“生产”使用实现接口,并在其后面使用真实的外部依赖项,
  • 为“测试”使用实现接口的“模拟”。
然后,在编写时:
  • 需要访问此资源的函数将其作为参数传递,
  • 需要访问此资源的方法将其作为参数或对象构造函数中的参数传递。
最后,在主函数中实例化生产依赖项,并从那里转发它们。
技巧而非诱惑:
  • 创建一个包含所有这些接口的Environment结构可能很有用,而不是向每个函数传递大量参数;然而,只需要一个或两个资源的函数应显式传递这些资源,以明确它们使用了什么,
  • 我发现过去将时间戳而不是时钟传递非常有用...因为多次调用now()可能会随着时间的推移返回不同的结果。

+1. 之前在/r/rust上看到的评论让我觉得Rust中不可能使用DI,很高兴现在发现它仍然是可行的。 - user9993
3
Rust没有依赖注入框架,但仍然可以手动传递依赖项。下一个障碍是泛型和引用。我建议使用Rc<RefCell<Trait>>或它的多线程版本来保存特性: 与CPU时间相比,外部依赖项速度较慢,额外的内存分配和虚拟调用在雷达上几乎不会产生影响。 - Matthieu M.
1
从我的经验来看,你可以采用不使用特质对象的方法。例如,当我测试应用程序协议时,我会编写针对Read/BufWrite特质的代码。但是,我并不传递特质对象,而是使用静态分发。这样可以在不引入任何运行时惩罚的情况下实现可测试性。 - Austin Jenkins

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接