Testing FS Access

Hello all,

I'm new to Rust, and I'm trying to create a small toy application that interacts with the File System.
I would like to write this is combination with unit tests, but here's a problem, how to test the File System.
In other languages, I would usually do it by mocking away the File System?

Is this the preferred approach in Rust as well?

Here's a sample that I created:

trait IOWorker<'a> {
    fn delete_directory(&mut self, path: &'a str) -> std::io::Result<()>;
}

struct StdIOWorker {}

impl<'a> IOWorker<'a> for StdIOWorker {
    fn delete_directory(&mut self, path: &'a str) -> std::io::Result<()> {
        std::fs::remove_dir_all(path)
    }
}

struct IOWorkerMock<'a> {
    deleted_directory: &'a str,
}

impl<'a> IOWorker<'a> for IOWorkerMock<'a> {
    fn delete_directory(&mut self, path: &'a str) -> std::io::Result<()> {
        self.deleted_directory = path;
        std::io::Result::Ok(())
    }
}

impl<'a> IOWorkerMock<'a>  {
    fn new() -> IOWorkerMock<'a> {
        IOWorkerMock {
            deleted_directory: ""
        }
    }
}

I can than write tests in the following way:

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

    #[test]
    fn test_delete_directory() {
        // SETUP.
        let mut io_worker = IOWorkerMock::new();

        // ARRANGE.
        let path = "C:\\Temp";

        // ACT.
        io_worker.delete_directory(path);

        // ASSERT.
        assert_eq!(io_worker.deleted_directory, path);
    }
}

Is this really the way it should be done in rust?
Any help or guidance is highly appreciated.

First, you absolutely do not want the lifetime on IOWorker. Change it to this:

trait IOWorker {
    fn delete_directory(&mut self, path: &str) -> std::io::Result<()>;
}

Generally introducing traits for something you otherwise wouldn't need traits for makes your life harder. I usually just see people actually access the file system in tests in a temporary directory instead of trying to mock it out.

Alternatively I see people testing the algorithms directly, and refactoring such that actual file system calls are outside that piece of code.

Thanks.

Can you explain why I don't want the lifetime on the IOWorker trait?
I'm new to Rust and I like to hear explanations why something is bad :slight_smile:

Your version tells the compiler that the IOWorker needs to retain a reference to path indefinitely. The compiler, then, will disallow code like this:

fn delete_wrapper(filesys:& mut impl IOWrapper) {
    let dir:String = find_target_directory();
    filesys.delete_directory(dir.as_str()).unwrap();
}

As far as the compiler is concerned, filesys might need to access dir’s memory later, but dir is destroyed when delete_wrapper exits. If it did, that would be a use-after-free bug.

1 Like

Thanks for that explanation.
I do have another error with my current implementation right here:

use std::io;

trait FileSystem {
    fn delete_directory(&mut self, path: &str) -> io::Result<()>;
}

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

    struct FSMock<'a> {
        deleted_directory: &'a str,
    }

    impl<'a> FSMock<'a>  {
        fn new() -> FSMock<'a> {
            FSMock {
                deleted_directory: "",
            }
        }
    }

    impl<'a> FileSystem for FSMock<'a> {
        fn delete_directory(&mut self, path: &str) -> io::Result<()> {
            self.deleted_directory = path;
            std::io::Result::Ok(())
        }
    }
}

I do have the following error when compiling:

lifetime of reference outlives lifetime of borrowed content...

How can I solve this particular issue?

I would recommend using a String as the type of deleted_directory. Otherwise you really do need the lifetime, and the variable with the path cannot be destroyed before the FileSystem object is destroyed as in @2e71828 example.

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.