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?
What is mocking? Mocking is a process used in unit testing when the unit being tested has external dependencies. The purpose of mocking is to isolate and focus on the code being tested and not on the behavior or state of external dependencies.
Mock Testing provides you the ability to isolate and test your code without any interference of the dependencies and other variables like network issues and traffic fluctuations. In simple words, in mock testing, we replace the dependent objects with mock objects.
At its simplest, a test in Rust is a function that's annotated with the test attribute. Attributes are metadata about pieces of Rust code; one example is the derive attribute we used with structs in Chapter 5. To change a function into a test function, add #[test] on the line before fn .
How would you design this or any similar code to make easier to unit testing and better design?
One way is to make get_some_data()
generic over the input stream. The std::io
module defines a Read
trait for all things you can read from, so it could look like this (untested):
use std::io::Read;
pub fn get_some_data(mut input: impl Read) -> Option<SomeData> {
let mut contents = String::new();
input.read_to_string(&mut contents).unwrap();
...
}
You'd call get_some_data()
with the input, e.g. get_some_data(File::open(file_name).unwrap())
or get_some_data(&mut io::stdin::lock())
, etc. When testing, you can prepare the input in a string and call it as get_some_data(io::Cursor::new(prepared_data))
.
As for the trait example, I think you misunderstood how to apply the pattern to your code. You're supposed to use the trait to decouple getting the data from processing the data, sort of how you'd use an interface in Java. The get_some_data()
function would receive an object known to implement the trait.
Code more similar to what you'd find in an OO language might choose to use a trait object:
trait ProvideData {
fn get_data(&self) -> String
}
struct FileData(PathBuf);
impl ProvideData for FileData {
fn get_data(&self) -> String {
std::fs::read(self.0).unwrap()
}
}
pub fn get_some_data(data_provider: &dyn ProvideData) -> Option<SomeData> {
let contents = data_provider.get_data();
...
}
// normal invocation:
// let some_data = get_some_data(&FileData("file name".into()));
In test you'd just create a different implementation of the trait - for example:
#[cfg(test)]
mod test {
struct StaticData(&'static str);
impl ProvideData for StaticData {
fn get_data(&self) -> String {
self.0.to_string()
}
}
#[test]
fn test_something() {
let some_data = get_some_data(StaticData("foo bar"));
assert!(...);
}
}
First of all, I would like to thank @user4815162342 for enlightenment of traits. Using his answer as base, i solve with my own solution for the problem.
First, I build as mention, traits to better design my code:
trait ProvideData {
fn get_data(&self) -> String
}
But I had some problems, since there were tons of bad design code, and lots code I had to mock before run the test, something like the below code.
pub fn some_function() -> Result<()> {
let some_data1 = some_non_deterministic_function(PathBuf::new())?;
let some_data2 = some_non_deterministic_function_2(some_data1);
match some_data2 {
Ok(ok) => Ok(()),
Err(err) => panic!("something went wrong"),
}
}
I would need to change almost all functions signatures to accept Fn
, this would not only change most my code, but will actually make it hard to read, since most of it I was changing for testing purpose only.
pub fn some_function(func1: Box<dyn ProvideData>, func2: Box<dyn SomeOtherFunction>) -> Result<()> {
let some_data1 = func1(PathBuf::new())?;
let some_data2 = func2(some_data1);
match some_data2 {
Ok(ok) => Ok(()),
Err(err) => panic!("something went wrong"),
}
}
Reading a little more deep the rust documentation, I slight changed the implementation.
trait ProvideData {
fn get_data(&self) -> String;
}
struct FileData(PathBuf);
impl ProvideData for FileData {
fn get_data(&self) -> String {
String::from(format!("Pretend there is something going on here with file {}", self.0.to_path_buf().display()))
}
}
new
functions for default implementation in the structs, and add constructor with default implementation using dynamic dispatch functions.
struct SomeData(Box<dyn ProvideData>);
impl SomeData {
pub fn new() -> SomeData {
let file_data = FileData(PathBuf::new());
SomeData {
0: Box::new(file_data)
}
}
pub fn get_some_data(&self) -> Option<String> {
let contents = self.0.get_data();
Some(contents)
}
}
fn main() {
//When the user call this function, it would no know that there is multiple implementations for it.
let some_data = SomeData::new();
assert_eq!(Some(String::from("Pretend there is something going on here with file ")),some_data.get_some_data());
println!("HEY WE CHANGE THE INJECT WITHOUT USER INTERATION");
}
And finally, since we test inside the declaration scope, we might change the injection even if is private:
mod test {
use super::*;
struct MockProvider();
impl ProvideData for MockProvider {
fn get_data(&self) -> String {
String::from("Mocked data")
}
}
#[test]
fn test_internal_data() {
let some_data = SomeData(Box::from(MockProvider()));
assert_eq!(Some(String::from("Mocked data")), some_data.get_some_data())
}
#[test]
fn test_ne_internal_data() {
let some_data = SomeData(Box::from(MockProvider()));
assert_ne!(Some(String::from("Not the expected data")), some_data.get_some_data())
}
}
The result code can be seem in the rust playground, hope this help user to design their code.
https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=62348977502accfed55fa4600d149bcd
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With