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

huangapple go评论87阅读模式
英文:

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: &amp;str) -&gt; String {
    let salt = rand::thread_rng().next_u32();

    let mut s = DefaultHasher::new();
    s.write_u32(salt);
    s.write(msg.as_bytes());
    format!(&quot;{:x}{:x}&quot;, &amp;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(&quot;foo&quot;), &quot;e80866501cdda8af09a0a656&quot;);
    }
}

Some approaches I've looked at:

  • I'm aware that the ThreadRng returned by rand::thread_rng() implements RngCore, so in theory I could set a variable somewhere to store a reference to a RngCore, 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&lt;u32&gt; so it can tell:

mod tests {
    use super::*;
    use lazy_static::lazy_static;
    use std::sync::Mutex;

    lazy_static! {
        static ref MOCK_SALT: Mutex&lt;Vec&lt;u32&gt;&gt; = Mutex::new(vec![]);
    }

    // Replaces random salt generation when testing.
    pub fn mock_salt() -&gt; 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(&quot;foo&quot;), &quot;e80866501cdda8af09a0a656&quot;);
    }
}

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: &amp;str) -&gt; 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!(&quot;{:x}{:x}&quot;, &amp;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.

Playground.

huangapple
  • 本文由 发表于 2020年1月6日 23:11:57
  • 转载请务必保留本文链接:https://go.coder-hub.com/59614507.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定