如何解决生锈的单元测试,模拟和特征
我目前正在构建一个高度依赖文件IO的应用程序,因此很明显,我的代码很多部分都有File::open(file)
。
做一些集成测试是可以的,我可以轻松设置文件夹以加载文件和所需的方案。
问题出在我要进行单元测试和代码分支的任何地方。我知道那里有很多声称可以进行模拟的模拟库,但是我觉得我最大的问题是代码设计本身。
例如,我将使用任何面向对象的语言(在示例中为java)执行相同的代码,我可以编写一些接口,并在测试中简单地覆盖我要模拟的默认行为,设置一个假{ {1}},无论是通过固定收益重新实现,还是使用诸如嘲笑之类的模拟框架。
ClientRepository
但是由于我们最终将数据与行为混合在一起,所以我无法在生锈中得到相同的结果。
在RefCell documentation上,有一个与我在java上给出的示例类似的示例。一些答案指向特征clojures,conditional 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(()),}
}
深入阅读锈文档,我略微更改了实现。
- 更改几乎所有代码以使用特征和结构(很多代码是公共函数)
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
函数,并使用动态分派函数添加具有默认实现的构造函数。
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");
}
最后,由于我们在声明范围内进行测试,因此即使是私有的,我们也可能会更改注入:
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())
}
}
结果代码可能会出现在锈迹斑斑的操场上,希望这有助于用户设计代码。
版权声明:本文内容由互联网用户自发贡献,该文观点与技术仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 dio@foxmail.com 举报,一经查实,本站将立刻删除。