【问题标题】:How can I test Rust methods that depend on environment variables?如何测试依赖于环境变量的 Rust 方法?
【发布时间】:2016-06-21 20:44:15
【问题描述】:

我正在构建一个库,该库会询问其运行环境以将值返回给询问程序。有时就像

一样简单
pub fn func_name() -> Option<String> {
    match env::var("ENVIRONMENT_VARIABLE") {
        Ok(s) => Some(s),
        Err(e) => None
    }
}

但有时会稍微复杂一些,甚至会产生由各种环境变量组成的结果。如何测试这些方法是否按预期运行?

【问题讨论】:

    标签: unit-testing environment-variables rust


    【解决方案1】:

    “我如何测试 X”几乎总是用“通过控制 X”来回答。在这种情况下,您需要控制环境变量:

    use std::env;
    
    fn env_is_set() -> bool {
        match env::var("ENVIRONMENT_VARIABLE") {
            Ok(s) => s == "yes",
            _ => false
        }
    }
    
    #[test]
    fn when_set_yes() {
        env::set_var("ENVIRONMENT_VARIABLE", "yes");
        assert!(env_is_set());
    }
    
    #[test]
    fn when_set_no() {
        env::set_var("ENVIRONMENT_VARIABLE", "no");
        assert!(!env_is_set());
    }
    
    #[test]
    fn when_unset() {
        env::remove_var("ENVIRONMENT_VARIABLE");
        assert!(!env_is_set());
    }
    

    但是,您需要注意环境变量是共享资源。来自the docs for set_var,强调我的:

    当前正在运行的进程的环境变量k设置为v

    您可能还需要注意,Rust 测试运行器默认并行运行测试,因此有可能让一个测试破坏另一个测试。

    此外,您可能希望在测试后将环境变量“重置”为已知的良好状态。

    【讨论】:

    • 我喜欢“通过控制 X”的答案。以后我肯定会使用它:)
    • @SimonWhitehead 它也适用于您的答案;在这种情况下,您正在控制对变量的 access 并使用依赖注入作为控制方法。 ^_^
    【解决方案2】:

    您的另一个选择(如果您不想搞乱实际设置环境变量)是抽象调用。我只是在学习 Rust,所以我不确定这是否是“Rust 方式(tm)”......但这肯定是我在另一种语言/环境中的做法:

    use std::env;
    
    pub trait QueryEnvironment {
        fn get_var(&self, var: &str) -> Result<String, std::env::VarError>;
    }
    
    struct MockQuery;
    struct ActualQuery;
    
    impl QueryEnvironment for MockQuery {
        fn get_var(&self, _var: &str) -> Result<String, std::env::VarError> {
            Ok("Some Mocked Result".to_string()) // Returns a mocked response
        }
    }
    
    impl QueryEnvironment for ActualQuery {
        fn get_var(&self, var: &str) -> Result<String, std::env::VarError> {
            env::var(var) // Returns an actual response
        }
    }
    
    fn main() {
        env::set_var("ENVIRONMENT_VARIABLE", "user"); // Just to make program execute for ActualQuery type
        let mocked_query = MockQuery;
        let actual_query = ActualQuery;
        
        println!("The mocked environment value is: {}", func_name(mocked_query).unwrap());
        println!("The actual environment value is: {}", func_name(actual_query).unwrap());
    }
    
    pub fn func_name<T: QueryEnvironment>(query: T) -> Option<String> {
        match query.get_var("ENVIRONMENT_VARIABLE") {
            Ok(s) => Some(s),
            Err(_) => None
        }
    }
    

    rust playground 上的示例

    注意 actual 调用是如何发生恐慌的。这是您将在 实际 代码中使用的实现。对于您的测试,您将使用模拟的。

    【讨论】:

    • 注意:您可以在参数/变量名称前加上下划线 _ 以避免警告它们未使用。这避免了使用指令#[allow(unused_variables)]
    • 谢谢@MatthieuM。 - 我总是忘记这一点!我的 Rust 之旅还处于起步阶段。
    【解决方案3】:

    第三种选择,我认为更好的选择是传入现有类型 - 而不是创建每个人都必须强制执行的新抽象。

    pub fn new<I>(vars: I)
        where I: Iterator<Item = (String, String)>
    {
        for (x, y) in vars {
            println!("{}: {}", x, y)
        }
    }
    
    #[test]
    fn trivial_call() {
        let vars = [("fred".to_string(), "jones".to_string())];
        new(vars.iter().cloned());
    }
    

    感谢#rust 上的 qrlpz 帮助我为我的程序排序,只是分享结果以帮助其他人:)

    【讨论】:

    • 看起来你想使用iter::empty
    • 或者切片中的内容 :) - 我主要关注函数,所以我只是放了一些数据,以便演示您如何在测试中使用它。
    【解决方案4】:

    编辑: 下面的测试助手现在可以在 dedicated crate

    中使用

    免责声明:我是合著者


    我也有同样的需求,并实现了一些小型测试助手来处理@Shepmaster 提到的注意事项。

    这些测试助手支持这样的测试:

    #[test]
    fn test_default_log_level_is_info() {
        with_env_vars(
            vec![
                ("LOGLEVEL", None),
                ("SOME_OTHER_VAR", Some("foo"))
            ],
            || {
                let actual = Config::new();
                assert_eq!("INFO", actual.log_level);
            },
        );
    }
    

    with_env_vars 将负责:

    • 在并行运行测试时避免副作用
    • 在测试闭包完成时将 env 变量重置为其原始值
    • 支持在测试关闭期间取消设置环境变量
    • 以上所有,当测试关闭出现恐慌时也是如此。

    帮手:

    use lazy_static::lazy_static;
    use std::env::VarError;
    use std::panic::{RefUnwindSafe, UnwindSafe};
    use std::sync::Mutex;
    use std::{env, panic};
    
    lazy_static! {
        static ref SERIAL_TEST: Mutex<()> = Default::default();
    }
    
    /// Sets environment variables to the given value for the duration of the closure.
    /// Restores the previous values when the closure completes or panics, before unwinding the panic.
    pub fn with_env_vars<F>(kvs: Vec<(&str, Option<&str>)>, closure: F)
    where
        F: Fn() + UnwindSafe + RefUnwindSafe,
    {
        let guard = SERIAL_TEST.lock().unwrap();
        let mut old_kvs: Vec<(&str, Result<String, VarError>)> = Vec::new();
        for (k, v) in kvs {
            let old_v = env::var(k);
            old_kvs.push((k, old_v));
            match v {
                None => env::remove_var(k),
                Some(v) => env::set_var(k, v),
            }
        }
    
        match panic::catch_unwind(|| {
            closure();
        }) {
            Ok(_) => {
                for (k, v) in old_kvs {
                    reset_env(k, v);
                }
            }
            Err(err) => {
                for (k, v) in old_kvs {
                    reset_env(k, v);
                }
                drop(guard);
                panic::resume_unwind(err);
            }
        };
    }
    
    fn reset_env(k: &str, old: Result<String, VarError>) {
        if let Ok(v) = old {
            env::set_var(k, v);
        } else {
            env::remove_var(k);
        }
    }
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-06-12
      • 1970-01-01
      • 2022-11-16
      • 1970-01-01
      • 2016-12-30
      • 1970-01-01
      • 2019-10-27
      相关资源
      最近更新 更多