rustmocking

How to mock std::something in Rust?


Let's say I have this:

fn something(path: String) {
     let contents: Vec<u8> = std::fs::read(path);
     ...
}

How to mock the call to std::fs::read()? I know Rust doesn't support mocking out of the box, but even using libs like mockall, I can't see how this could be done.

These libs can mock traits and structs defined by me, because I can annotate those, but I don't see how this is supposed to work with the standard library modules.

Maybe mockall is not the right tool? What lib should I use?


Solution

  • You can conditionally select which read to use a few different ways.

    Mock function

    In this example we define our own read only if tests are being built, otherwise we import std::fs::read. Then we can just use read and know we'll get one or the other depending on whether tests are being built.

    use std::path::Path;
    
    #[cfg(not(test))]
    use std::fs::read;
    
    #[cfg(test)]
    fn read(p: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
        todo!()
    }
    
    fn example() {
        let data = read("path").unwrap();
    }
    
    fn main() {
        example();
    }
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn example() {
            super::example();
        }
    }
    

    Conditionally invoke mock function at call site

    In this case we use the cfg! macro to decide which function to call at the call site. The optimizer will eliminate the other branch as dead code.

    The disadvantage to this approach is having to duplicate the condition at every location you need to invoke read. The advantage is it's very clear to the reader that mocking happens during tests without having to inspect the rest of the file to see what read is.

    use std::path::Path;
    
    fn mock_read(p: impl AsRef<Path>) -> std::io::Result<Vec<u8>> {
        todo!()
    }
    
    fn example() {
        let data = if cfg!(test) {
            mock_read("path")
        } else {
            std::fs::read("path")
        }
        .unwrap();
    }
    
    fn main() {
        example();
    }
    
    #[cfg(test)]
    mod tests {
        #[test]
        fn example() {
            super::example();
        }
    }