在测试时模拟单个函数有哪些模式?



我有一个函数为某些数据生成加盐哈希摘要。对于盐,它使用随机u32值。它看起来像这样:

use rand::RngCore;
use std::collections::hash_map::DefaultHasher;
use std::hash::Hasher;
fn hash(msg: &str) -> String {
let salt = rand::thread_rng().next_u32();
let mut s = DefaultHasher::new();
s.write_u32(salt);
s.write(msg.as_bytes());
format!("{:x}{:x}", &salt, s.finish())
}

在测试中,我想验证它是否在给定已知盐和字符串的情况下生成预期值。我如何模拟(旋转?rand::thread_rng().next_u32()测试中生成特定值?换句话说,什么可以替换此示例中的注释以使测试通过?

mod tests {
#[test]
fn test_hashes() {
// XXX How to mock ThreadRng::next_u32() to return 3892864592?
assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
}
}

我看过的一些方法:

  • 我知道rand::thread_rng()返回的ThreadRng实现了RngCore,所以理论上我可以在某处设置一个变量来存储对RngCore的引用,并在测试期间实现我自己的模拟变体。我在 Go 和 Java 中采用了这种方法,但我无法让 Rust 类型检查器允许它。

  • 我查看了模拟框架的列表,例如 MockAll,但它们似乎旨在模拟要传递给方法的结构或特征,并且此代码不传递一个,我不一定希望库的用户能够传入RngCore

  • 使用#[cfg(test)]宏调用 tests 模块中指定的其他函数,然后让该函数读取要从其他位置返回的值。我开始工作,但不得不使用一个不安全的可变静态变量来设置要查找的模拟方法的值,这似乎很恶心。有没有更好的方法?

作为参考,我将使用#[cfg(test)]+ 不安全的可变静态变量技术发布一个答案,但希望有一种更直接的方法可以做到这一点。

在测试模块中,使用lazy-static 添加一个带有互斥锁的静态变量以确保线程安全,创建一个像next_u32()这样的函数来返回其值,并让测试将静态变量设置为已知值。如果未设置,它应该回退到返回一个正确的随机数,所以在这里我把它Vec<u32>,以便它可以告诉:

mod tests {
use super::*;
use lazy_static::lazy_static;
use std::sync::Mutex;
lazy_static! {
static ref MOCK_SALT: Mutex<Vec<u32>> = Mutex::new(vec![]);
}
// Replaces random salt generation when testing.
pub fn mock_salt() -> u32 {
let mut sd = MOCK_SALT.lock().unwrap();
if sd.is_empty() {
rand::thread_rng().next_u32()
} else {
let ret = sd[0];
sd.clear();
ret
}
}
#[test]
fn test_hashes() {
MOCK_SALT.lock().unwrap().push(3892864592);
assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
}
}

然后在测试时修改hash()以调用tests::mock_salt()而不是rand::thread_rng().next_u32()(函数体的前三行是新的(:

fn hash(msg: &str) -> String {
#[cfg(test)]
let salt = tests::mock_salt();
#[cfg(not(test))]
let salt = rand::thread_rng().next_u32();
let mut s = DefaultHasher::new();
s.write_u32(salt);
s.write(msg.as_bytes());
format!("{:x}{:x}", &salt, s.finish())
}

然后使用宏允许 Rust 在编译时确定要调用哪个函数,因此在非测试构建中不会降低效率。这确实意味着源代码中有一些关于测试模块的知识,但它不包含在二进制文件中,所以应该相对安全。我想可能有一个自定义派生宏以某种方式自动化。像这样:

#[mock(rand::thread_rng().next_u32())]
let salt = rand::thread_rng().next_u32();

当然,将在测试模块(或其他地方?(中自动生成模拟方法,将其插入此处,并为测试提供函数以仅在测试时设置值---。不过,似乎很多。

操场。

相关内容

  • 没有找到相关文章

最新更新