5

I have troubles figuring out unit tests for the methods of the target struct.

I have a method random_number that returns a random value based on the attribute of the struct and there is another method plus_one that takes the result of the first method and does something with it:

pub struct RngTest {
    pub attr: u64,
}

impl RngTest {
    pub fn random_number(&self) -> u64 {
        let random = 42; // lets pretend it is random
        return random * self.attr;
    }

    pub fn plus_one(&self) -> u64 {
        return self.random_number() + 1;
    }
}

Having a unit test for the first method, what is the strategy to test the other? I want to mock self.random_number() output for the unit test of plus_one() to have sane code in unit tests. There is a nice post that compares different mocking libraries and concludes (sadly enough) that none of them is really good to stand out from the others.

The only thing I learned while reading instructions for these libraries is that the only way I can mock methods is by moving them to a trait. I didn't see any example in these libraries (I looked at 4 or 5 of them) where they test a case similar to this.

After moving these methods to a trait (even as they are), how do I mock random_number to unit test the output of RngTest::plus_one?

pub trait SomeRng {
    fn random_number(&self) -> u64 {
        let random = 42; // lets pretend it is random
        return random * self.attr;
    }

    fn plus_one(&self) -> u64 {
        return self.random_number() + 1;
    }
}

impl SomeRng for RngTest {}
1
  • Don't make sense in Rust ? Commented Mar 14, 2019 at 0:11

3 Answers 3

8

How to mock specific methods but not all of them in Rust?

As you have already learned, you cannot replace methods on a type. The only thing you can do is move the methods to a trait and then provide production and test-specific implementations of that trait. How you structure the trait determines the granularity of what you are able to test.

Trait with a default implementation

Depending on your use case, you might be able to use a default implementation:

trait SomeRng {
    fn random_number(&self) -> u64;

    fn plus_one(&self) -> u64 {
        self.random_number() + 1
    }
}

struct RngTest(u64);
impl SomeRng for RngTest {
    fn random_number(&self) -> u64 {
        self.0
    }
}

#[test]
fn plus_one_works() {
    let rng = RngTest(41);
    assert_eq!(rng.plus_one(), 42);
}

Here, random_number is a required method, but plus_one has a default implementation. Implementing random_number gives you plus_one by default. You could also choose to implement plus_one if you could do it more efficiently.

What does the real rand crate do?

The real rand crate uses two traits:

  • Rng

    pub trait Rng: RngCore { /* ... */ }
    

    An automatically-implemented extension trait on RngCore providing high-level generic methods for sampling values and other convenience methods.

  • RngCore

    pub trait RngCore { /* ... */ }
    

    The core of a random number generator.

This splits the core interesting parts of the implementation from the helper methods. You can then control the core and test the helpers:

trait SomeRngCore {
    fn random_number(&self) -> u64;
}

trait SomeRng: SomeRngCore {
    fn plus_one(&self) -> u64 {
        self.random_number() + 1
    }
}

impl<R: SomeRngCore> SomeRng for R {}

struct RngTest(u64);
impl SomeRngCore for RngTest {
    fn random_number(&self) -> u64 {
        self.0
    }
}

#[test]
fn plus_one_works() {
    let rng = RngTest(41);
    assert_eq!(rng.plus_one(), 42);
}
Sign up to request clarification or add additional context in comments.

Does ` impl<R: SomeRngCore> SomeRng for R {} ` mean every Struct impl SomeRngCore will have SomeRng's methods too ?
Could you please answer Charlie 木匠's question? And if trait SomeRng: SomeRngCore extends SomeRngCore, why would you also need impl<R: SomeRngCore> SomeRng for R {}? I looked in the book trying to understand but couldn't find the answer. Or ... oh, one is defining a mandatory trait extension, the other a mandatory impl extension, perhaps?
Having experimented, I think this is amazingly clever stuff. But isn't it slightly open to the criticism that you are deliberately complicating the app code to cater for your testing aims? The app code may well not need this "extended trait" structure (if that's what it is). I'm assuming that all that is inescapable due to the nature of the Rust language ... but it's a bit unsatisfactory.
3

Instead of defining a stub as Shepmaster done in their answer, you can use the crate mockall to define a real mock (more flexible). You still have to follow the struct + trait pattern as Shepmaster proposed:

use mockall::mock;

pub struct RngTest {
    pub attr: u64,
}

trait SomeRng {
    fn random_number(&self) -> u64;

    fn plus_one(&self) -> u64 {
        self.random_number() + 1
    }
}

// Your real implementation
impl SomeRng for RngTest {
    fn random_number(&self) -> u64 {
        42
    }
}


// Test part

mock! {
    SomeRngForTest {}
    impl SomeRng for SomeRngForTest {
        fn random_number(&self) -> u64;
    }
}


mod rngtest {
    use super::{SomeRng, MockSomeRngForTest};

    #[test]
    fn test() {
        let mut mock = MockSomeRngForTest::new();
        mock.expect_random_number().return_const(100u64);

        assert_eq!(mock.plus_one(), 101);
    }
}

See: https://docs.rs/mockall/latest/mockall/

For my money this is a better solution than that of the Mighty Shepmaster... simply because there is no "distorting" the app code for the purpose of testing. Other rules may necessarily apply in Rust since testing is significantly harder to do than some other languages, but to me it feels problematic to introduce changes to the app code which are not justified by the app code's inherent requirements.
0

Thanks to @Shepmaster I came to this workaround. I have added the actual Rng to have more context.

use rand::{thread_rng, Rng}; // 0.6.5

struct RngTest(Vec<u64>);

impl RngTest {
    fn random_number(&self) -> u64 {
        let random_value = thread_rng().choose(&self.0);
        *random_value.unwrap()
    }

    fn plus_one(&self) -> u64 {
        self.random_number() + 1
    }
}

#[test]
fn plus_one_works() {
    let rng = RngTest(vec![1]);
    assert_eq!(rng.plus_one(), 2);
}

I can set an appropriate value in the object and don't need to use traits. There is a downside though - this forces me to have a special instance of my type for this specific test which I would like to avoid because my actual type has a lot of fields and I wanted to define its creation once for all the tests using speculate.

Comments

Your Answer

Draft saved
Draft discarded

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.