【问题标题】:Is there a cleaner way to test functions that use functions that require user input in Rust?有没有一种更简洁的方法来测试使用需要用户在 Rust 中输入的函数的函数?
【发布时间】:2020-03-04 04:32:38
【问题描述】:

我正在为我的第一个 Rust 项目编写 CLI question asking library,因为无论如何我都可能会使用它,但我找不到一个干净的方法来测试构建器模式的 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();
        }
    }
}

寻找解决方案我发现了dependency injection,它允许我为提示用户使用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);
}

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

【问题讨论】:

  • 它不允许我将每个输入的输入更改为函数 - 你能澄清一下你的意思吗?
  • 因为reader、writer 和question 都在confirm() 函数内部传递给prompt_user() 我无法像在测试prompt_user() 时那样操作输入功能本身。所以我无法自动化涉及confirm()的测试。
  • 那么,你是说没有任何参数的confirm 不支持依赖注入?如果是这样,我不确定我们能提供什么帮助。是的,你必须有一些注入依赖项的方法,参数是最简单的,但你也可以有结构字段。是什么阻止您在 confirm 上使用参数来注入您需要的依赖项?
  • 我不希望confirm 有任何争论,就像公共界面设计的问题一样。我什至没有想过在confirm 中使用结构字段作为prompt_user 的参数。我认为这可行,让我在测试时修改输入源。

标签: unit-testing rust


【解决方案1】:

如 Stack Overflow question you linked 中所述,如果您想要可测试性,通常应避免硬连接外部依赖项(也称为 I/O):

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

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

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

那么,写的时候:

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

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


技巧,而不是款待:

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

【讨论】:

  • +1。以前在 /r/rust 上创建的 cmets 给我的印象是,在 Rust 中 DI 是不可能的,很高兴看到它仍然是可能的。
  • @user9993:Rust 没有依赖注入框架,但仍然可以手动传递依赖。下一个障碍是通用性和引用,我建议使用 Rc&lt;RefCell&lt;Trait&gt;&gt; 或其多线程对应物来保存特征:无论如何,与 CPU 时间相比,外部依赖关系很慢,额外的内存分配和虚拟调用几乎不会是一个小问题雷达。
  • 根据我的经验,您可以在不使用特征对象的情况下采用这种方法。例如,当我测试应用程序协议时,我将针对 Read/BufWrite 特征进行编写。然而,我没有传递特征对象,而是使用静态调度。您可以在不引入任何运行时损失的情况下获得可测试性。
猜你喜欢
  • 2017-05-20
  • 2020-05-30
  • 1970-01-01
  • 2021-04-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-27
相关资源
最近更新 更多