Rust: Mocking rand functions with gen_range

Viewed 448

I am writing a small program that randomly selects an entry from an enum. Sample code:

#[derive(Debug)]
enum SettlementSize {
    VILLAGE,
    TOWN,
    CITY
}

impl Distribution<SettlementSize> for Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> SettlementSize {
        let res = rng.gen_range(0, 3);
        match res {
            0 => SettlementSize::VILLAGE,
            1 => SettlementSize::TOWN,
            2 => SettlementSize::CITY,
            _ => panic!("Unknown value!")
        }
    }
}

fn get_settlement_size(mut rng: impl RngCore) -> SettlementSize {
    let size: SettlementSize = rng.gen();
    size
}

Now, of course I want to test it. That's why get_settlement_size takes the rng value.

    #[test]
    fn random_human_readable() {
        let rng = StepRng::new(1, 1);
        assert_eq!("Town", get_settlement_size(rng).human_readable());
    }

Unfortunately, this doesn't work. When I added some printlns, the value returned from:

rng.gen_range(0, 3);

is always 0. I copied StepRng code into my test module to add println inside and I see next_u32 and next_u64 called. However, later the code disappears into UniformSampler and at that point it becomes too hard for me to follow. What am I doing wrong? Can I somehow retain the testability (which means being able to set fixed results for random in my mind)?

1 Answers

You're right, it is easy to mock the primitive functions of the RngCore trait, but the way they are used to avoid bias from low order bits and bias from modulus calculations make it very tricky to mock the more complicated functions in RngCore.

IMO the simplest way to approach this is to place a layer between your use of the Rng and the code you want to test.

So instead of this:

fn get_settlement_size(mut rng: impl RngCore) -> SettlementSize {
    let size: SettlementSize = rng.gen();
    size
}

you have

trait RngWrapper {
    fn get_settlement_size(&mut self) -> SettlementSize;
}

fn get_settlement_size(rng: &mut impl RngWrapper) -> SettlementSize {
    rng.get_settlement_size()
}

Now your "real" implementation looks like this

impl Distribution<SettlementSize> for Standard {
    fn sample<R: Rng + ?Sized>(&self, rng: &mut R) -> SettlementSize {
        let res = rng.gen_range(0..3);
        match res {
            0 => SettlementSize::VILLAGE,
            1 => SettlementSize::TOWN,
            2 => SettlementSize::CITY,
            _ => panic!("Unknown value!"),
        }
    }
}

struct Random<'a, R: RngCore> {
    rng: &'a mut R,
}

impl<'a, R> RngWrapper for Random<'a, R>
where
    R: RngCore,
{
    fn get_settlement_size(&mut self) -> SettlementSize {
        self.rng.gen()
    }
}

And your mock could look something like this:

struct AlwaysTown {}

impl RngWrapper for AlwaysTown {
    fn get_settlement_size(&mut self) -> SettlementSize {
        SettlementSize::TOWN
    }
}

Now you can test anything that uses get_settlement_size(), but you've not addressed testing of Random<'_, ThreadRng>::get_settlement_size - However this is now an isolated issue and doesn't require the "being able to set fixed results for random" that you mention in your question - instead I'd do a statistical test - that each of the expected cases comes out roughly the expected number of times. If you want this rigorous, then you need a bit of stats (which I'm not going to put here) - but you should be able to put together a rough test that will pass 99.99+% of the time.

A full example, except the statistical tests on RngCore, is on the playground.

Related