Mocking functions in rust
Asked Answered
D

2

5

Is there a way to mock regular functions in rust?

Consider the following code:

fn main() {
    println!("{}", foo());
}

fn get_user_input() -> u8 {
    // Placeholder for some unknown value
    42
}

fn foo() -> u8 {
    get_user_input()
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_foo() {
        use super::*;

        get_user_input = || 12u8;

        assert_eq!(foo(), 12u8);
    }
}

I would like to unit test foo() without having to rely on the output of get_user_input(). I obviously cannot overwrite get_user_input() like I tried in the example code.

I have only found ways to mock structs, traits and modules but nothing about mocking regular free functions. Am I missing something?

Edit: I have looked primarily at the mockall crate.

Duumvir answered 6/9, 2022 at 10:7 Comment(1)
The way you have written the code, foo() is unconditionally calling get_user_input(), and there is nothing you can do purely in the test to change that. You need to write your code in a way to allow injecting a different function. The easiest way is to define a trait, say UserInput, with an assoicated function get(), and make foo() generic over T: UserInput. You can then have a standard implementation for UserInput for the actual code, and another one just for the tests.Gley
R
8

You could use cfg:

#[cfg(not(test))]
fn get_user_input() -> u8 {
    // Placeholder for some unknown value
    42
}

#[cfg(test)]
fn get_user_input() -> u8 {
    12
}

playground

Or dependency injection:

pub fn main() {
    println!("{}", foo(get_user_input));
}

fn get_user_input() -> u8 {
    // Placeholder for some unknown value
    42
}

fn foo(get_user_input_: impl Fn() -> u8) -> u8 {
    get_user_input_()
}

#[cfg(test)]
mod tests {
    #[test]
    fn test_foo() {
        use super::*;
        
        let get_user_input = || 12u8;

        assert_eq!(foo(get_user_input), 12u8);
    }
}

playgound

Roseberry answered 6/9, 2022 at 12:12 Comment(1)
Yes, this is way to do it. Unfortunately it appears you have to do that in the actual file of the struct/impl concerned, i.e. by the nature of what you're doing you can't mock in a separate test file (i.e. under dir "tests" in the module).Solstice
S
-1

"Mocking" in the sense of examining which parameters have been passed to a function

Using crate mockall, here's a possibility for mocking a function from an outside module which is used by the internal methods/functions of your app code.

My outside module (in fact a self-contained crate) is called "utilities". It contains a function I want to mock to examine and run checks on the parameters passed to it, called reqwest_call (simplified in what follows). Also see crate reqwest: the main Rust workhorse equivalent to Python requests package.

Cargo.toml of the crate which uses crate utilities has to have the following dependencies:

[dependencies]
...
mockall_double = "0.3.1"

[dev-dependencies]
...
mockall = "0.12.1"

(the main mockall crate does NOT have to be included in releases. But the smaller one, mockall_double, does).

What follows is all happening at the bottom of the file containing the struct-impl you want to test (i.e. it doesn't work in the separate "tests" directory of the module).

use mockall_double::double;
// alias the imported function 
use utilities::reqwest_call as real_reqwest_call;
// let mockall create a "double"
#[allow(unused_imports)]
#[double]
use test_utilities::reqwest_call;

// set up mocking
mod test_utilities {
    use super::*;
    
    #[allow(dead_code)]
    #[cfg(not(test))]
    pub fn reqwest_call(url: &str, method: reqwest::Method)
        -> Result<ResponseObject> {
        // here is where the REAL method gets called in the case of a run (not testing)
        real_reqwest_call(url, method)
    }
    
    #[allow(dead_code)]
    #[cfg(test)]
    pub fn mock_reqwest_call(url: &str, method: reqwest::Method)
        -> Result<ResponseObject> {
        // this is where your parameter-testing code goes
        assert!(url.contains(...));
        ...
        Ok(ResponseObject::new()) // for example
    }
}

// the mod containing the tests
#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_something() -> Result() {
        ... 
        let _ = tds.do_bulk_insert();
        ...
    

In the actual app code the method do_bulk_insert() calls reqwest_call at some point... maybe even several times. During an app run the real function gets called (via real_reqwest_call).

But during a test run the configured function mock_reqwest_call, in mod test_utilities, gets called instead each time. Note that mockall creates the "mock" function mock_reqwest_call (with the characters "mock_" prepended) automatically.

If the parameters passed fail the assert!s in the function mock_reqwest_call, the test fails.

Yes, quite a rigmarole compared to pytest. And as I say, this won't work in files under directory "tests", because cfg(test) is always false in these files (in fact use of a feature might provide a way to get around that, but that's beyond the scope of this answer).

Another limitation is that this checking of the parameters will run in the same way in all tests. What you need is the ability to branch conditionally, inside the mocked function, depending on the name of the currently running test. There has been some discussion on this but it's not yet implemented as far as I'm aware (someone please suggest an edit if you know better).

As it happens, an acceptable workaround in my example is not too difficult to find, however: for example, the param url can be engineered to reveal which test is being run when the mock function is called. However each case will be different: human ingenuity may be required to contrive to make sure your mocked function can be passed a parameter (during testing) which somehow reveals which test is being run.

(NB again, the use of a feature might be a way of switching on and off mocking for given test functions ... but again, kind of beyond the scope of this answer).

Solstice answered 10/5 at 19:9 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.