【问题标题】:Creating an `std::env::Args` iterator for testing创建一个 `std::env::Args` 迭代器进行测试
【发布时间】:2018-05-06 13:55:31
【问题描述】:

Rust 有没有办法从 Vec<String> 创建一个 std::env::Args 以便在 #[test] 函数中使用它?

我希望测试一个以std::env::Args 作为参数的函数,但我不知道如何使用我为测试提供的参数列表创建这样的对象。

我无法从文档、来源或 Google 搜索中找出这一点。

【问题讨论】:

  • 你能改变这个函数,让它使用一个通用的I: Iterator<Item = String>吗?这更容易模拟。
  • @trentcl,很好的解决方法,但这是惯用的 Rust 吗?我必须放弃这个函数期望获取命令行参数的明确意图,转而支持这个函数期望一些生成字符串的通用迭代器使其可测试吗?
  • 在你接受的东西中保持通用是要走的路。函数名称可以表明意图,但使用通用输入类型可以保持函数的灵活性。
  • 是吗?如果函数的契约是“接受命令行参数”,那么测试它的唯一正确方法是传递命令行参数。您必须放宽合同才能使用任意字符串对其进行测试。 (但我应该提到IntoIterator 而不是Iterator,因为它适用于Vec。)
  • 除此之外,我倾向于同意 loganfsmyth,但如果出于某种原因在调用者上强制执行“给我命令行参数”很重要,您可以将通用函数及其测试包装在一个模块中并且只公开一个pub fn,它完全采用Args(并调用通用版本来完成工作)。虽然在这一点上也许你还不如自己打电话给std::env::args(),根本不接受任何争论。

标签: unit-testing rust parameter-passing


【解决方案1】:

std::env::Args 的字段没有被记录,而且似乎没有一个公共函数可以使用自定义字段创建一个。所以,你运气不好。

但由于它只是“进程参数的迭代器,为每个参数产生一个 String”您的函数可以采用 String 迭代器或 Vec 而不会造成任何损失功能或类型安全。由于它只是Strings 的列表,因此将函数任意限制为恰好来自命令行的字符串没有多大意义。

浏览Rust's own tests,他们就是这么做的。有很多let args: Vec<String> = env::args().collect();

甚至还有an example in rustbuild,他们去掉程序的名称,只提供参数列表。

use std::env;

use bootstrap::{Config, Build};

fn main() {
    let args = env::args().skip(1).collect::<Vec<_>>();
    let config = Config::parse(&args);
    Build::new(config).build();
}

bootstrap::Config::parse() 看起来像这样:

impl Config {
    pub fn parse(args: &[String]) -> Config {
        let flags = Flags::parse(&args);
        ...

我不是 Rust 专家,但这似乎是 Rust 人处理问题的方式。

【讨论】:

    【解决方案2】:

    @Schwern's answer 很好,它让我找到了这个更简单的版本。由于std::env::Args 使用Item = String 实现Iterator,您可以这样做:

    use std::env;
    
    fn parse<T>(args: T)
    where
        T: Iterator<Item = String>,
    {
        for arg in args {
            // arg: String
            print!("{}", arg);
        }
    }
    
    fn main() {
        parse(env::args());
    }
    

    要进行测试,您需要为 parse 提供一个在 String 之上的迭代器:

    #[test]
    fn test_parse() {
       let args = ["arg1", "arg2"].iter().map(|s| s.to_string());
       parse(args);
    }
    

    【讨论】:

    • FWIW,这已在现有答案中提到:“您的函数可以采用 String 迭代器”。
    • 使用IntoIterator 可能会在某些时候使代码更简单(fn parse(args: impl IntoIterator&lt;Item = String&gt;))。要非常灵活,请接受任何可以视为&amp;str (fn parse(args: impl IntoIterator&lt;Item = impl AsRef&lt;str&gt;&gt;)) 或Into&lt;String&gt; 的内容。
    【解决方案3】:

    我基于@Rossman's answer(因此也基于@Schwern's answer;感谢两者)编写了一个小宏来简化此操作:

    macro_rules! make_string_iter {
        ($($element: expr), *) => {
            {
                let mut v = Vec::new();
                $( v.push(String::from($element)); )*
                v.into_iter()
            }
        };
    }
    

    可以这样使用:

    macro_rules! make_string_iter {
        ($($element: expr), *) => {
            {
                let mut v = Vec::new();
                $( v.push(String::from($element)); )*
                v.into_iter()
            }
        };
    }
    
    // We're using this function to test our macro
    fn print_args<T: Iterator<Item = String>>(args: T) {
        for item in args {
            println!("{}", item);
        }
    }
    
    fn main() {
    
        // Prints a, b and c
        print_args(make_string_iter!("a", "b", "c"))
    }
    

    或在Rust Playground 上试用。

    我(还)不是 rust 专家,欢迎提出任何建议 :)

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2016-07-13
      • 2016-07-24
      • 2023-03-05
      • 2021-10-15
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2018-05-20
      相关资源
      最近更新 更多