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?