首页 > 解决方案 > rust 中的单元测试、模拟和特征

问题描述

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

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

问题来自我想要的单元测试和代码分支。我知道有很多模拟库声称可以模拟,但我觉得我最大的问题是代码设计本身。

比方说,我会用任何面向对象的语言(示例中的 java)编写相同的代码,我可以编写一些接口,并在测试中简单地覆盖我想要模拟的默认行为,设置 a fake 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 上给出的示例类似的示例。一些答案指向特征,clojures条件编译

我们可能会在测试中提供一些场景,第一个是一些 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(())
    }
}

第二个相同的功能成为特征并且一些结构实现它。




#[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,当然,这可能会扩展到任何非确定性函数,如时间、数据库连接等。

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

标签: unit-testingrustmockingtddtraits

解决方案


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

一种方法是对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())orget_some_data(&mut io::stdin::lock())等​​。在测试时,您可以将输入准备为字符串并将其称为get_some_data(io::Cursor::new(prepared_data)).

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

与您在 OO 语言中找到的更相似的代码可能会选择使用特征对象

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

推荐阅读