生锈的单元测试,模拟和特征

如何解决生锈的单元测试,模拟和特征

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

做一些集成测试是可以的,我可以轻松设置文件夹以加载文件和所需的方案。

问题出在我要进行单元测试和代码分支的任何地方。我知道那里有很多声称可以进行模拟的模拟库,但是我觉得我最大的问题是代码设计本身。

例如,我将使用任何面向对象的语言(在示例中为java)执行相同的代码,我可以编写一些接口,并在测试中简单地覆盖我要模拟的默认行为,设置一个假{ {1}},无论是通过固定收益重新实现,还是使用诸如嘲笑之类的模拟框架。

ClientRepository

但是由于我们最终将数据与行为混合在一起,所以我无法在生锈中得到相同的结果。

RefCell documentation上,有一个与我在java上给出的示例类似的示例。一些答案指向特征clojuresconditional compiliation

我们可能会测试一些场景,首先是一些mod.rs中的公共功能


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
  }
}

第二个相同的函数变成一个特征,并通过某些struct实现它。



#[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(())
    }
}

在两个示例中,由于要绑定 #[derive(Serialize,Clone)] pub struct SomeData { pub name: 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(()) } } ,因此我们很难对其进行测试,并且可以肯定的是,这可能会扩展到任何不确定性函数,例如时间,数据库连接等。

您将如何设计此代码或任何类似的代码,以简化单元测试和更好的设计?

很抱歉,很长的帖子。

~~马铃薯图像~~

解决方法

您将如何设计此代码或任何类似的代码,以简化单元测试和更好的设计?

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

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();
    ...
}

您将使用输入来呼叫get_some_data(),例如get_some_data(File::open(file_name).unwrap())get_some_data(&mut io::stdin::lock())等。测试时,您可以准备字符串形式的输入,并将其称为get_some_data(io::Cursor::new(prepared_data))

关于特征示例,我认为您误解了如何将模式应用于代码。您应该使用特征来使获取数据与处理数据脱钩,这类似于您在Java中使用接口的方式。 get_some_data()函数将接收一个已知可以实现特征的对象。

与您在OO语言中发现的代码更相似的代码可能选择使用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()));

在测试中,您将创建特征的另一种实现-例如:

#[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!(...);
    }
}
,

首先,我要感谢@ user4815162342对特性的启发。以他的回答为基础,我用自己的解决方案解决了这个问题。

首先,如上所述,我构建了一些特征来更好地设计我的代码:


trait ProvideData {
    fn get_data(&self) -> String
}

但是我遇到了一些问题,因为有很多糟糕的设计代码,并且在运行测试之前我不得不模拟很多代码,例如下面的代码。


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

我将需要更改几乎所有的函数签名才能接受Fn,这不仅会更改我的大部分代码,而且实际上会使它难以阅读,因为大部分更改仅出于测试目的。


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(()),}
}

深入阅读锈文档,我略微更改了实现。

  1. 更改几乎所有代码以使用特征和结构(很多代码是公共函数)

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()))
    }
}

  1. 为结构中的默认实现添加一个new函数,并使用动态分派函数添加具有默认实现的构造函数。

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

  1. 由于构造函数是私有的,因此可以防止用户注入代码,并且可以出于测试目的自由更改内部实现,并且集成测试保持平稳运行。


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

最后,由于我们在声明范围内进行测试,因此即使是私有的,我们也可能会更改注入:


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())
    }
}

结果代码可能会出现在锈迹斑斑的操场上,希望这有助于用户设计代码。

https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=62348977502accfed55fa4600d149bcd

版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。

相关推荐


依赖报错 idea导入项目后依赖报错,解决方案:https://blog.csdn.net/weixin_42420249/article/details/81191861 依赖版本报错:更换其他版本 无法下载依赖可参考:https://blog.csdn.net/weixin_42628809/a
错误1:代码生成器依赖和mybatis依赖冲突 启动项目时报错如下 2021-12-03 13:33:33.927 ERROR 7228 [ main] o.s.b.d.LoggingFailureAnalysisReporter : *************************** APPL
错误1:gradle项目控制台输出为乱码 # 解决方案:https://blog.csdn.net/weixin_43501566/article/details/112482302 # 在gradle-wrapper.properties 添加以下内容 org.gradle.jvmargs=-Df
错误还原:在查询的过程中,传入的workType为0时,该条件不起作用 &lt;select id=&quot;xxx&quot;&gt; SELECT di.id, di.name, di.work_type, di.updated... &lt;where&gt; &lt;if test=&qu
报错如下,gcc版本太低 ^ server.c:5346:31: 错误:‘struct redisServer’没有名为‘server_cpulist’的成员 redisSetCpuAffinity(server.server_cpulist); ^ server.c: 在函数‘hasActiveC
解决方案1 1、改项目中.idea/workspace.xml配置文件,增加dynamic.classpath参数 2、搜索PropertiesComponent,添加如下 &lt;property name=&quot;dynamic.classpath&quot; value=&quot;tru
删除根组件app.vue中的默认代码后报错:Module Error (from ./node_modules/eslint-loader/index.js): 解决方案:关闭ESlint代码检测,在项目根目录创建vue.config.js,在文件中添加 module.exports = { lin
查看spark默认的python版本 [root@master day27]# pyspark /home/software/spark-2.3.4-bin-hadoop2.7/conf/spark-env.sh: line 2: /usr/local/hadoop/bin/hadoop: No s
使用本地python环境可以成功执行 import pandas as pd import matplotlib.pyplot as plt # 设置字体 plt.rcParams[&#39;font.sans-serif&#39;] = [&#39;SimHei&#39;] # 能正确显示负号 p
错误1:Request method ‘DELETE‘ not supported 错误还原:controller层有一个接口,访问该接口时报错:Request method ‘DELETE‘ not supported 错误原因:没有接收到前端传入的参数,修改为如下 参考 错误2:cannot r
错误1:启动docker镜像时报错:Error response from daemon: driver failed programming external connectivity on endpoint quirky_allen 解决方法:重启docker -&gt; systemctl r
错误1:private field ‘xxx‘ is never assigned 按Altʾnter快捷键,选择第2项 参考:https://blog.csdn.net/shi_hong_fei_hei/article/details/88814070 错误2:启动时报错,不能找到主启动类 #
报错如下,通过源不能下载,最后警告pip需升级版本 Requirement already satisfied: pip in c:\users\ychen\appdata\local\programs\python\python310\lib\site-packages (22.0.4) Coll
错误1:maven打包报错 错误还原:使用maven打包项目时报错如下 [ERROR] Failed to execute goal org.apache.maven.plugins:maven-resources-plugin:3.2.0:resources (default-resources)
错误1:服务调用时报错 服务消费者模块assess通过openFeign调用服务提供者模块hires 如下为服务提供者模块hires的控制层接口 @RestController @RequestMapping(&quot;/hires&quot;) public class FeignControl
错误1:运行项目后报如下错误 解决方案 报错2:Failed to execute goal org.apache.maven.plugins:maven-compiler-plugin:3.8.1:compile (default-compile) on project sb 解决方案:在pom.
参考 错误原因 过滤器或拦截器在生效时,redisTemplate还没有注入 解决方案:在注入容器时就生效 @Component //项目运行时就注入Spring容器 public class RedisBean { @Resource private RedisTemplate&lt;String
使用vite构建项目报错 C:\Users\ychen\work&gt;npm init @vitejs/app @vitejs/create-app is deprecated, use npm init vite instead C:\Users\ychen\AppData\Local\npm-