我目前正在构建一个应用程序,它严重依赖于文件IO,因此我的代码中显然有很多部分使用了File::open(file)
.
做一些集成测试还好,我可以轻松设置文件夹以加载需要的文件和场景。
问题在于当我想要进行单元测试和代码分支测试时。我知道有很多模拟库声称可以进行模拟,但我感觉我最大的问题是代码设计本身。
例如,如果我在任何面向对象的语言(例如Java)中执行相同的代码,则可以编写一些接口,并在测试中简单地覆盖我要模拟的默认行为,设置一个虚假的ClientRepository
,重新实现固定返回值,或者使用一些模拟框架,如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
}
}
但是我在Rust中无法获得相同的结果,因为我们最终会将数据与行为混合在一起。
在RefCell文档中,有一个类似于我在Java中提供的示例。有些答案指向特征(traits),闭包(closures),条件编译(conditional compilation)
我们可能会遇到一些测试方案,第一个是某个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(())
}
}
一旦相同的功能成为一个特征(trait),一些结构体(struct)就会实现它。
#[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(())
}
}
在这两个例子中,我们很难进行单元测试,因为我们与File::open
绑定在一起,而且很明显,这可能会扩展到任何非确定性函数,如时间、数据库连接等。
您将如何设计此类代码,使其更易于单元测试并具有更好的设计?