How to mock `std::fs::File` so can check if `File::set_len` was used correctly in unit tests?

Viewed 1136

As I check File::set_len(..) looks like it's implemented for struct File , but not via Trait.

Goal: test foo that takes file open as read/write , performs operations of : reads, writes, seeks, and trimming file to certain size. We like to provide initial state of file in test, and check result. Preferably in-memory.

How to test code that relies on set_len? (io::Seek or other traits didn't help so far).

I would like to mock it.

Let's make a toy example, to make discussion easier:

#![allow(unused_variables)]

use std::error::Error;
use std::fs::File;
use std::io::Cursor;

// assumes that file is open in Read/Write mode
// foo performs reads and writes and Seeks
// at the end wants to trim size of file to certain size.
fn foo(f: &mut File) -> Result<(), Box<dyn Error>> {
    f.set_len(0)?;
    Ok(())
}

fn main () -> Result<(), Box<dyn Error>> {
    let mut buf = Vec::new();
    let mut mockfile = Cursor::new(&buf);
    // we would like to supply foo
    // with "test" representation of initial file state
    foo(&mut mockfile)
    // and check afterwards if resulting contents (=> size)
    // of file match expectations
}

on rust-play : https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=950a94504168d51f043966288fae3bca

Error:

error[E0308]: mismatched types
  --> src/main.rs:15:9
   |
15 |     foo(&mut mockfile)
   |         ^^^^^^^^^^^^^ expected struct `File`, found struct `std::io::Cursor`

P.S. before receiving answers I started giving a shot to tempfile crate: https://docs.rs/tempfile/3.1.0/tempfile/#structs . Still, ideal solution is "in-memory" so can't wait for answers to question :).

1 Answers

In short, you can't mock std::fs::File given a function that requires that exact type - it's just not how Rust works.

However, if you have control over foo, you can easily invent a trait that has set_len and make foo generic over that trait. Since it's your trait, you can implement it for types defined elsewhere (such as File), which will make foo() accept File as it did before. But it will also accept anything else that implements the trait, including the mock types you create in the test suite. And thanks to monomorphization, its execution will be just as efficient as the original code. For example:

pub trait SetLen {
    fn set_len(&mut self, len: u64) -> io::Result<()>;
}

impl SetLen for File {
    fn set_len(&mut self, len: u64) -> io::Result<()> {
        File::set_len(self, len)
    }
}

pub fn foo(f: &mut impl SetLen) -> Result<(), Box<dyn Error>> {
    f.set_len(0)?;
    Ok(())
}

// You can always pass a `File` to `foo()`:
fn main() -> Result<(), Box<dyn Error>> {
    let mut f = File::create("bla")?;
    foo(&mut f)?;
    Ok(())
}

To mock it, you would just define a type that implements the trait and records whether it's been called:

#[derive(Debug, Default)]
struct MockFile {
    set_len_called: Option<u64>,
}

impl SetLen for MockFile {
    fn set_len(&mut self, len: u64) -> io::Result<()> {
        self.set_len_called = Some(len);
        Ok(())
    }
}

#[test]
fn test_set_len_called() {
    let mut mf = MockFile::default();
    foo(&mut mf).unwrap();
    assert!(mf.set_len_called == Some(0));
}

Playground

Related