Iam current building a application which heavy relies on File IO, so obviously lots of parts of my code have File::open(file).
Doing some integration tests are ok, I can easily set folders to load file and scenarios needed for it.
The problem comes whatever I want to unit tests, and code branches. I know there is lots of mocking libraries out there that claim to mocks, but i feel my biggest problem is code design itself.
Let's say for instance, I would do the same code in any object oriented language (java in the example), i could write some interfaces, and on tests simple override the default behavior I want to mock, set the a fake ClientRepository, whatever reimplemented wih a fixed return, or use some mocking framework, like mockito.
public interface ClientRepository {
   Client getClient(int id)
}
public class ClientRepositoryDB {
   
  private ClientRepository repository;
  
  //getters and setters
  public Client getClientById(int id) {
    Client client = repository.getClient(id);
    //Some data manipulation and validation
  }
}
But i couldn`t manage to get the same results in rust, since we endup mixing data with behavior.
On the RefCell documentation, there is a similar example with the one I gave on java. Some of answers points to traits, clojures, conditional compiliation
We might come with some scenarios in test, first one a public function in some mod.rs
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SomeData {
    pub name: Option<String>,
    pub address: Option<String>,
}
pub fn get_some_data(file_path: PathBuf) -> Option<SomeData> {
    let mut contents = String::new();
    match File::open(file_path) {
        Ok(mut file) => {
            match file.read_to_string(&mut contents) {
                Ok(result) => result,
                Err(_err) => panic!(
                    panic!("Problem reading file")
                ),
            };
        }
        Err(err) => panic!("File not find"),
    }
    
    // using serde for operate on data output
    let some_data: SomeData = match serde_json::from_str(&contents) {
        Ok(some_data) => some_data,
        Err(err) => panic!(
            "An error occour when parsing: {:?}",
            err
        ),
    };
    //we might do some checks or whatever here
    Some(some_data) or None
}
mod test {
    use super::*;
    
    #[test]
    fn test_if_scenario_a_happen() -> std::io::Result<()> {
       //tied with File::open
       let some_data = get_some_data(PathBuf::new);
        assert!(result.is_some());
        Ok(())
    }
    #[test]
    fn test_if_scenario_b_happen() -> std::io::Result<()> {
       //We might need to write two files, and we want to test is the logic, not the file loading itself
       let some_data = get_some_data(PathBuf::new);
        assert!(result.is_none());
        Ok(())
    }
}
The second the same function becoming a trait and some struct implement it.
#[derive(Serialize, Deserialize, Debug, Clone)]
pub struct SomeData {
   pub name: Option<String>,
   pub address: Option<String>,
}
trait GetSomeData {
   fn get_some_data(&self, file_path: PathBuf) -> Option<SomeData>;
}
pub struct SomeDataService {}
impl GetSomeData for SomeDataService {
   fn get_some_data(&self, file_path: PathBuf) -> Option<SomeData> {
       let mut contents = String::new();
       match File::open(file_path) {
           Ok(mut file) => {
               match file.read_to_string(&mut contents) {
                   Ok(result) => result,
                   Err(_err) => panic!("Problem reading file"),
               };
           }
           Err(err) => panic!("File not find"),
       }
       // using serde for operate on data output
       let some_data: SomeData = match serde_json::from_str(&contents) {
           Ok(some_data) => some_data,
           Err(err) => panic!("An error occour when parsing: {:?}", err),
       };
       //we might do some checks or whatever here
       Some(some_data) or None
   }
}
impl SomeDataService {
   pub fn do_something_with_data(&self) -> Option<SomeData> {
       self.get_some_data(PathBuf::new())
   }
}
mod test {
   use super::*;
   
   #[test]
   fn test_if_scenario_a_happen() -> std::io::Result<()> {
      //tied with File::open
      let service = SomeDataService{}
      let some_data = service.do_something_with_data(PathBuf::new);
       assert!(result.is_some());
       Ok(())
   }
}
On both examples, we have a hard time unit testing it, since we tied with File::open, and surely, this might be extend to any non-deterministic function, like time, db connection, etc.
How would you design this or any similar code to make easier to unit testing and better design?