How can I test Rust methods that depend on environment variables?

Viewed 6231

I am building a library that interrogates its running environment to return values to the asking program. Sometimes as simple as

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

but sometimes a good bit more complicated, or even having a result composed of various environment variables. How can I test that these methods are functioning as expected?

4 Answers

EDIT: The test helpers below are now available in a dedicated crate

Disclaimer: I'm a co-author


I had the same need and implemented some small test helpers which take care of the caveats mentioned by @Shepmaster .

These test helpers enable testing like so:

#[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 will take care of:

  • Avoiding side effects when running tests in parallel
  • Resetting the env variables to their original values when the test closure completes
  • Support for unsetting environment variables during the test closure
  • All of the above, also when the test-closure panics.

The helper:

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