Kla*_*ein 5 tdd unit-testing mocking traits 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
}
}
Run Code Online (Sandbox Code Playgroud)
但是我无法在 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(())
}
}
Run Code Online (Sandbox Code Playgroud)
第二个相同的函数成为一个特征并且一些结构实现了它。
#[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(())
}
}
Run Code Online (Sandbox Code Playgroud)
在这两个例子中,我们很难对它进行单元测试,因为我们绑定了File::open,当然,这可能会扩展到任何非确定性函数,如时间、数据库连接等。
您将如何设计此代码或任何类似的代码,以便更轻松地进行单元测试和更好的设计?
抱歉,帖子太长了。
~~土豆图~~
您将如何设计这段代码或任何类似的代码,以便更容易进行单元测试和更好的设计?
一种方法是对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();
...
}
Run Code Online (Sandbox Code Playgroud)
您可以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 语言中的代码更相似的代码可能会选择使用特征对象:
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()));
Run Code Online (Sandbox Code Playgroud)
在测试中,您只需创建该特征的不同实现 - 例如:
#[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!(...);
}
}
Run Code Online (Sandbox Code Playgroud)
首先感谢@user4815162342对trait的启发。以他的答案为基础,我用自己的解决方案解决了这个问题。
首先,我构建了提及的特征,以更好地设计我的代码:
trait ProvideData {
fn get_data(&self) -> String
}
Run Code Online (Sandbox Code Playgroud)
但我遇到了一些问题,因为有大量糟糕的设计代码,并且在运行测试之前我必须模拟很多代码,例如下面的代码。
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"),
}
}
Run Code Online (Sandbox Code Playgroud)
我需要更改几乎所有函数签名以接受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(()),
Err(err) => panic!("something went wrong"),
}
}
Run Code Online (Sandbox Code Playgroud)
更深入地阅读 Rust 文档,我稍微改变了实现。
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()))
}
}
Run Code Online (Sandbox Code Playgroud)
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)
}
}
Run Code Online (Sandbox Code Playgroud)
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");
}
Run Code Online (Sandbox Code Playgroud)
最后,由于我们在声明范围内进行测试,因此即使是私有的,我们也可能会更改注入:
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");
}
Run Code Online (Sandbox Code Playgroud)
结果代码可以在 Rust Playground 中看到,希望这可以帮助用户设计他们的代码。
| 归档时间: |
|
| 查看次数: |
3654 次 |
| 最近记录: |