What's the recommended way of testing I/O functions?


#1

… in particular when it comes to the file I/O. I actually have a function like

    pub fn update(job: Job, file: &str) -> Result<(), failure::Error> {
        let db_raw = fs::read_to_string(file)
            .context(format!("Could not read the job database, {}", file))?;
        let mut db: JobDB = toml::from_str(&db_raw)?;
        db.push(job);
        let db_raw = toml::to_string_pretty(&db).expect("New DB malformed");
        fs::write(file, db_raw).context("Could not write the new DB")?;
        Ok(())
    }

One way could be create an auxiliary function that would accept a String and return a String but this won’t catch any bugs in the I/O handling. One could have a file some temporary directory but this sounds like an easy way to lose platform-independence.


#2

You can change the fn to be generic over Read + Write (rather than hardcoding fs::read_to_string/fs::write) and then provide a test/mock Read/Write impl that induces cases you’d like to test.


#3

The tempfile crate provides platform-independent temporary directory APIs.


#4

In my business logic I’ll usually mock out all IO by pulling the IO operation out into its own trait (or reuse an existing trait like Read or Write), that way you get faster and more reliable tests. It also helps decouple the components in your application.

Testing error handling and various edge cases can then be done by passing in a specially crafted mock (e.g. a Write implementation which always errors) and seeing what happens.

If you’re testing IO code itself you pretty much need to do the operation (e.g. Rust std implementing File::write()). Typically you’d execute the IO in a temporary environment (e.g. tempfile or a db you spin up just for that test) where you don’t care about rogue tests breaking the world.

As an anecdote, one of the things we do at work is control actual mechanical machines and my initial reaction was to mock things out so the library is decoupled/insulated from the real world. Then one of the other engineers pointed out that the only way to make sure the machine moves when you hit a button is by actually hitting a button and seeing the machine move… That said, this is still done in a “safe” testing environment and not on customer machines.

Obviously you’re just using that as an example, but if tempfile behaved differently on Windows to Linux I’d say its a bug in the tempfile crate. Whatever you’re using to create a temporary environment/mock db/whatever should present a platform-independent interface to its users wherever possible.