如何测试依赖于环境变量的 Rust 方法?
How can I test Rust methods that depend on environment variables?
我正在构建一个库,该库将其 运行 环境询问为询问程序的 return 值。有时就像
一样简单
pub fn func_name() -> Option<String> {
match env::var("ENVIRONMENT_VARIABLE") {
Ok(s) => Some(s),
Err(e) => None
}
}
但有时会更复杂一些,甚至会产生由各种环境变量组成的结果。我如何测试这些方法是否按预期运行?
"How do I test X" 几乎总是用 "by controlling 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
,强调我的:
Sets the environment variable k
to the value v
for the currently running process.
您可能还需要注意 Rust 测试运行器默认并行运行测试,因此一个测试可能会破坏另一个测试。
此外,您可能希望在测试后"reset"您的环境变量达到已知的良好状态。
你的另一个选择(如果你不想搞乱实际设置环境变量)是抽象调用。我只是刚刚学习 Rust,所以我不确定这是否是“Rust 的方式 (tm)”...但这肯定是我在另一个 language/environment:[=12= 中的做法]
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
}
}
上的示例
注意 actual 调用是如何恐慌的。这是您将在 actual 代码中使用的实现。对于您的测试,您将使用模拟的。
第三种选择,也是我认为更好的选择,是传入现有类型 - 而不是创建每个人都必须强制使用的新抽象。
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 帮助我对我的程序进行排序,分享结果以帮助其他人:)
编辑:
下面的测试助手现在可以在 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);
}
}
我正在构建一个库,该库将其 运行 环境询问为询问程序的 return 值。有时就像
一样简单pub fn func_name() -> Option<String> {
match env::var("ENVIRONMENT_VARIABLE") {
Ok(s) => Some(s),
Err(e) => None
}
}
但有时会更复杂一些,甚至会产生由各种环境变量组成的结果。我如何测试这些方法是否按预期运行?
"How do I test X" 几乎总是用 "by controlling 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
,强调我的:
Sets the environment variable
k
to the valuev
for the currently running process.
您可能还需要注意 Rust 测试运行器默认并行运行测试,因此一个测试可能会破坏另一个测试。
此外,您可能希望在测试后"reset"您的环境变量达到已知的良好状态。
你的另一个选择(如果你不想搞乱实际设置环境变量)是抽象调用。我只是刚刚学习 Rust,所以我不确定这是否是“Rust 的方式 (tm)”...但这肯定是我在另一个 language/environment:[=12= 中的做法]
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
}
}
上的示例
注意 actual 调用是如何恐慌的。这是您将在 actual 代码中使用的实现。对于您的测试,您将使用模拟的。
第三种选择,也是我认为更好的选择,是传入现有类型 - 而不是创建每个人都必须强制使用的新抽象。
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 帮助我对我的程序进行排序,分享结果以帮助其他人:)
编辑: 下面的测试助手现在可以在 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);
}
}