如何测试依赖环境变量的Rust方法?

17

我正在构建一个库,它会查询其运行环境以返回值给询问程序。有时候这很简单,比如

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

但有时候情况会更为复杂,甚至结果可能由多个环境变量组成。我该如何测试这些方法是否按照预期运行?

但有时候情况会更为复杂,甚至结果可能由多个环境变量组成。我该如何测试这些方法是否按照预期运行?
4个回答

30

“如何测试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());
}

但是,请注意环境变量是一个共享资源。从set_var的文档(强调我的)中可以了解:

将环境变量k设置为值v,针对当前正在运行的进程

您还需要知道Rust测试运行器默认并行运行测试,因此可能会有一个测试覆盖另一个测试。

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


2
我喜欢“通过控制X”的答案。我以后一定会使用它 :) - Simon Whitehead
1
@SimonWhitehead,这同样适用于您的回答;在这种情况下,您正在控制对变量的访问,并使用依赖注入作为控制方法。^_^ - Shepmaster
这个是否已经过时了?似乎在某些情况下,env::var可能会产生错误,如果环境变量不是有效的Unicode。或者这是一个有意的决定,因为非Unicode环境变量应该被视为“未设置”,以便正确处理它? - hyperupcall
@hyperupcall 是的,这是对这个答案的有意决定。如果您需要使用非 UTF-8 环境变量,请改用 env::var_os - Shepmaster
3
我编写了一个脚本,用于运行1000次 cargo test 的示例。它成功运行了896次,失败了104次。可能是因为测试是并行运行的,存在在设置变量和测试之间的竞态条件。我认为,目前的答案实际上没有回答这个问题,因为它没有提供一种实际测试环境变量的方法。看到这个答案的人可能会忽略这一点,并最终遇到很多烦恼的不稳定测试。 - Neil Roberts
@NeilRoberts 这些问题已经在回答中明确解决了(并且已经解决了7年)。我同意共享全局状态是不好的,我基本上会避免在我的项目中使用这种代码风格(特别是考虑到 set_env 可以秘密引入 内存不安全!)。相反,我努力在程序开头一次性获取所有环境变量,然后构建一个配置数据结构。那个结构就是我用于测试的。 - Shepmaster

13

编辑: 以下测试辅助程序现在可以在专用的创建中使用。

免责声明:我是共同作者之一。


我有同样的需求,并实现了一些小的测试辅助程序,可以处理 @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 能够自动处理以下事项:

  • 在并行运行测试时避免副作用。
  • 在测试闭包完成后将环境变量重置为其原始值。
  • 支持在测试闭包期间取消设置环境变量。
  • 即使测试闭包发生 panic,也能够执行以上所有操作。

该辅助函数:

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

9

如果你不想折腾设置环境变量,另一种选择是将调用封装起来。我刚开始学习 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示例中,注意到实际调用会导致panic。这是您在实际代码中使用的实现。对于测试,您将使用模拟的实现。


注意:您可以在参数/变量名称前加下划线 _,以避免警告它们未使用。这样可以避免使用指令 #[allow(unused_variables)] - Matthieu M.
谢谢@MatthieuM。-我总是忘记那个!我在Rust之旅中仍然很早。 - Simon Whitehead

0
第三个选项,我认为更好的选择是传递现有类型 - 而不是创建一个每个人都必须强制转换的新抽象。
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 - Shepmaster
或者在切片中的内容 :) - 我主要关注于函数本身,所以我只是放了一些数据进去,以便演示如何在测试中使用它。 - lifeless

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