英文:
What patterns exist for mocking a single function while testing?
问题
以下是翻译好的部分:
我有一个函数,用于为一些数据生成加盐哈希摘要。对于盐,它使用一个随机的 `u32` 值。代码看起来像这样:
``` rust
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())
}
在一个测试中,我想验证它是否在已知的盐和字符串情况下生成了预期的值。在测试中,如何模拟 (swizzle?) rand::thread_rng().next_u32()
以生成特定的值?换句话说,这个示例中的注释应该用什么来替代,以使测试通过?
mod tests {
#[test]
fn test_hashes() {
// XXX 如何模拟 ThreadRng::next_u32() 返回 3892864592?
assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
}
}
我查看了一些方法:
-
我知道
rand::thread_rng()
返回的ThreadRng
实现了RngCore
,所以从理论上讲,我可以在某个地方设置一个变量来存储对RngCore
的引用,并在测试期间实现自己的模拟变量。我在 Go 和 Java 中采用了这种方法,但是我无法让 Rust 类型检查器接受它。 -
我查看了一些模拟框架的列表,比如 MockAll,但它们似乎是设计用来模拟传递给方法的结构或特性的,而此代码不会传递一个,并且我不一定希望库的用户能够传递
RngCore
。 -
使用
#[cfg(test)]
宏来调用在测试模块中指定的不同函数,然后让该函数从其他地方读取要返回的值。这个方法我设法让它工作了,但是不得不使用不安全的可变静态变量来设置供模拟方法使用的值,这看起来有些不太好。是否有更好的方法?
作为参考,我将发布一个使用 #[cfg(test)]
+ 不安全可变静态变量技术的答案,但希望有一种更简单的方法来做这种事情。
<details>
<summary>英文:</summary>
I have a function generates a salted hash digest for some data. For the salt, it uses a random `u32` value. It looks something like this:
``` rust
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())
}
In a test, I'd like to validate that it produces expected values, given a known salt and string. How do I mock (swizzle?) rand::thread_rng().next_u32()
in the test to generate a specific value? In other words, what could replace the comment in this example to make the test pass?
mod tests {
#[test]
fn test_hashes() {
// XXX How to mock ThreadRng::next_u32() to return 3892864592?
assert_eq!(hash("foo"), "e80866501cdda8af09a0a656");
}
}
Some approaches I've looked at:
-
I'm aware that the
ThreadRng
returned byrand::thread_rng()
implementsRngCore
, so in theory I could set a variable somewhere to store a reference to aRngCore
, and implement my own mocked variant to set during testing. I've taken this sort of approach in Go and Java, but I couldn't get the Rust type checker to allow it. -
I looked at the list of mock frameworks, such as MockAll, but they appear to be designed to mock a struct or trait to pass to a method, and this code doesn't pass one, and I wouldn't necessarily want users of the library to be able to pass in a
RngCore
. -
Use the
#[cfg(test)]
macro to call a different function specified in the tests module, then have that function read the value to return from elsewhere. This I got to work, but had to use an unsafe mutable static variable to set the value for the mocked method to find, which seems gross. Is there a better way?
As a reference, I'll post an answer using the #[cfg(test)]
+ unsafe mutable static variable technique, but hope there's a more straightforward way to do this sort of thing.
答案1
得分: 2
In the test module, 使用 lazy-static 来添加一个带有 Mutex 的静态变量以实现线程安全性,创建一个名为 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![]);
}
// 在测试时替换随机盐生成。
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();
会自动生成测试模块中的模拟方法(或其他地方?),并提供函数来设置值 - 仅在测试时才这样做。不过这看起来有点复杂。
英文:
In the test module, use lazy-static to add a static variable with a Mutex for thread safety, create a function like next_u32()
to return its value, and have tests set the static variable to a known value. It should fall back on returning a properly random number if it's not set, so here I've made it Vec<u32>
so it can tell:
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");
}
}
Then modify hash()
to call tests::mock_salt()
instead of rand::thread_rng().next_u32()
when testing (the first three lines of the function body are new):
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())
}
Then use of the macros allows Rust to determine, at compile time, which function to call, so there's no loss of efficiency in non-test builds. It does mean that there's some knowledge of the tests module in the source code, but it's not included in the binary, so should be relatively safe. I suppose there could be a custom derive macro to automate this somehow. Something like:
#[mock(rand::thread_rng().next_u32())]
let salt = rand::thread_rng().next_u32();
Would auto-generate the mocked method in the tests module (or elsewhere?), slot it in here, and provide functions for the tests to set the value --- only when testing, of course. Seems like a lot, though.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论