Unit testing 单元测试、模拟和锈蚀特性

Unit testing 单元测试、模拟和锈蚀特性,unit-testing,rust,mocking,tdd,traits,Unit Testing,Rust,Mocking,Tdd,Traits,我目前正在构建一个严重依赖文件IO的应用程序,因此显然我的代码的很多部分都有File::open(File) 做一些集成测试是可以的,我可以很容易地设置文件夹来加载文件和它所需的场景 无论我想做什么,问题都会出现:单元测试和代码分支。我知道有很多模拟库声称可以进行模拟,但我觉得我最大的问题是代码设计本身 比如说,我会用任何面向对象的语言(示例中的java)编写相同的代码,我可以编写一些接口,在测试中简单地覆盖我想要模拟的默认行为,设置一个假的ClientRepository,无论用固定的返回重新

我目前正在构建一个严重依赖文件IO的应用程序,因此显然我的代码的很多部分都有
File::open(File)

做一些集成测试是可以的,我可以很容易地设置文件夹来加载文件和它所需的场景

无论我想做什么,问题都会出现:单元测试和代码分支。我知道有很多模拟库声称可以进行模拟,但我觉得我最大的问题是代码设计本身

比如说,我会用任何面向对象的语言(示例中的java)编写相同的代码,我可以编写一些接口,在测试中简单地覆盖我想要模拟的默认行为,设置一个假的
ClientRepository
,无论用固定的返回重新实现什么,或者使用一些模拟框架,比如mockito


公共接口ClientRepository{
客户端getClient(int-id)
}
公共类ClientRepositoryDB{
私人客户存储库;
//接球手和接球手
公共客户端getClientById(int id){
Client-Client=repository.getClient(id);
//一些数据操作和验证
}
}
但我无法在rust中获得同样的结果,因为我们最终将数据与行为混为一谈

另一方面,我在java上给出了一个类似的例子。一些答案指向性格

我们可能会在测试中出现一些场景,首先是一些mod.r中的公共函数


#[派生(序列化、反序列化、调试、克隆)]
pub结构SomeData{
酒吧名称:Option,
发布地址:选项,
}
pub fn获取一些数据(文件路径:PathBuf)->选项{
让mut contents=String::new();
匹配文件::打开(文件路径){
确定(mut文件)=>{
匹配文件。读取到字符串(&mut内容){
Ok(结果)=>结果,
呃(_Err)=>恐慌(
惊慌失措!(“读取文件时出现问题”)
),
};
}
Err(Err)=>panic!(“找不到文件”),
}
//使用serde对数据输出进行操作
让some_data:SomeData=匹配serde_json::from_str(&contents){
Ok(一些数据)=>一些数据,
Err(Err)=>恐慌(
“分析:{:?}时出现错误”,
犯错误
),
};
//我们可以在这里做些检查或其他什么
部分(部分数据)或无
}
模试验{
使用超级::*;
#[测试]
fn测试如果场景发生()->std::io::结果{
//与文件绑定::打开
让一些数据=获取一些数据(PathBuf::new);
断言!(result.is_some());
好(())
}
#[测试]
fn测试如果场景发生()->std::io::结果{
//我们可能需要编写两个文件,我们要测试的是逻辑,而不是文件加载本身
让一些数据=获取一些数据(PathBuf::new);
断言!(result.is_none());
好(())
}
}
第二种方法是将相同的函数变成特征,然后由某个结构实现它


#[派生(序列化、反序列化、调试、克隆)]
pub结构SomeData{
酒吧名称:Option,
发布地址:选项,
}
特征获取数据{
fn获取一些数据(&self,文件路径:PathBuf)->选项;
}
发布结构SomeDataService{}
为SomeDataService导入GetSomeData{
fn获取一些数据(&self,文件路径:PathBuf)->选项{
让mut contents=String::new();
匹配文件::打开(文件路径){
确定(mut文件)=>{
匹配文件。读取到字符串(&mut内容){
Ok(结果)=>结果,
Err(_Err)=>panic!(“读取文件时出现问题”),
};
}
Err(Err)=>panic!(“找不到文件”),
}
//使用serde对数据输出进行操作
让some_data:SomeData=匹配serde_json::from_str(&contents){
Ok(一些数据)=>一些数据,
Err(Err)=>panic!(“解析时出错:{:?}”,Err),
};
//我们可以在这里做些检查或其他什么
部分(部分数据)或无
}
}
impl SomeDataService{
pub fn使用数据(&self)->选项做某事{
获取一些数据(PathBuf::new())
}
}
模试验{
使用超级::*;
#[测试]
fn测试如果场景发生()->std::io::结果{
//与文件绑定::打开
让服务=SomeDataService{}
让some_data=service.do_something_使用_数据(PathBuf::new);
断言!(result.is_some());
好(())
}
}
在这两个例子中,我们都很难对其进行单元测试,因为我们使用了
File::open
,当然,这可能会扩展到任何非确定性函数,如时间、数据库连接等

您将如何设计此代码或任何类似的代码以使单元测试更容易和更好地设计

抱歉发了这么长的邮件

~~马铃薯图像~~

您将如何设计此代码或任何类似的代码以使单元测试更容易和更好地设计

一种方法是在输入流上使
get_some_data()
generic。
std::io
模块为您可以读取的所有内容定义了一个
Read
特征,因此它可能看起来像这样(未测试):

使用std::io::Read;
pub fn获取一些数据(mut输入:impl Read)->选项{
让mut contents=String::new();
input.read_to_字符串(&mut contents).unwrap();
...
}
您可以使用输入调用
get_some_data()
,例如
get_some_data(File::open(File\u name).unwrap())
get_some_data(&mut io::stdin::lock())
。测试时,您可以准备字符串形式的输入,并将其称为
get_some_data(io::Cursor::new(preparented_data))

至于trait示例,我认为您误解了如何将模式应用于代码。您应该使用trait将获取数据与处理数据解耦,就像在Java中使用接口一样。
get\u some\u data()
函数将接收一个已知的对象来实现该特性

代码与OO语言中的代码更相似
#[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!(...);
    }
}


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");
}