0

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:

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 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.

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
theory
  • 9,178
  • 10
  • 59
  • 129
  • Thanks for the links to dupes, @Shepmaster. I think, reading through those, I can eventually understand a Rust-ier way to go about it, I'm not fluent enough yet that they're super obvious. Your section on methods in [this answer](https://stackoverflow.com/a/28370712/79202) looks closest to what I imagine would be best; I'll work though its application to my example and post a link to a playground here once I'm happy with it. – theory Jan 06 '20 at 16:56
  • Sounds good! You may also be interested in [How can I avoid a ripple effect from changing a concrete struct to generic?](https://stackoverflow.com/q/44912349/155423) to avoid external API changes just to enable dependency injection for testing. – Shepmaster Jan 06 '20 at 16:58

1 Answers1

2

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.

Playground.

theory
  • 9,178
  • 10
  • 59
  • 129
  • [How do I create a global, mutable singleton?](https://stackoverflow.com/q/27791532/155423) – Shepmaster Jan 06 '20 at 15:32
  • [applied](https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=2104fdc60e01ae4e0a992293f424d5fb) – Shepmaster Jan 06 '20 at 15:42
  • Have to admit I don't follow the application of the mutable singleton answer here, although [lazy-static](https://github.com/rust-lang-nursery/lazy-static.rs) looks like it'd be a nice replacement for the unsafe stuff I have here. Curious what fluent Rustaceans would make of this macro approach, though. – theory Jan 06 '20 at 16:59
  • 1
    Your `static mut MOCK_SALT` *is* a global mutable singleton. Using `lazy_static!` and a `Mutex` avoids the unsafety. In fact, your code is *probably introducing undefined behavior*, especially considering all tests are run in separate threads. This is why `unsafe` shouldn't be used lightly. – Shepmaster Jan 06 '20 at 17:03
  • Updated to use `lazy_static!` and a `Mutex` for thread safety. – theory Jan 13 '20 at 20:18