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

时间:2016-03-08 03:21:58

标签: unit-testing environment-variables rust

我正在构建一个查询其运行环境的库,以便将值返回给ask程序。有时候就像

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

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

4 个答案:

答案 0 :(得分:8)

“我如何测试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测试运行器默认情况下会并行运行测试,因此可能会有另一个测试clobber。

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

答案 1 :(得分:5)

你的另一个选择(如果你不想搞乱实际设置环境变量)是抽象出来的。我只是在学习Rust,所以我不确定这是不是“Rust way(tm)”这样做...但这肯定是我会在另一种语言/环境中做到的:

use std::env;

pub trait QueryEnvironment {
    fn get_var(&self, var: &'static str) -> Result<String, std::env::VarError>;
}

struct MockQuery;
struct ActualQuery;

impl QueryEnvironment for MockQuery {
    #[allow(unused_variables)]
    fn get_var(&self, var: &'static 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: &'static str) -> Result<String, std::env::VarError> {
        env::var(var) // Returns an actual response
    }
}

fn main() {
    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
    }
}

围栏的示例:http://is.gd/QhUlDW

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

答案 2 :(得分: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帮助我为我的程序排序,只需分享结果以帮助其他人:)

答案 3 :(得分:0)

我有同样的需求,并实现了一些小型测试助手,它们处理了@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);
    }
}