这个问题在这里已经有了答案:
How to mock external dependencies in tests? [duplicate]
(1 个回答)
How to mock specific methods but not all of them in Rust?
(2 个回答)
How can I test stdin and stdout?
(1 个回答)
Is there a way of detecting whether code is being called from tests in Rust?
(1 个回答)
What is the proper way to use the `cfg!` macro to choose between multiple implementations?
(1 个回答)
1年前关闭。
我目前正在构建一个严重依赖文件 IO 的应用程序,所以很明显我的代码的很多部分都有 File::open(file)
.
做一些集成测试是可以的,我可以很容易地设置文件夹来加载它需要的文件和场景。
问题来自我想要的单元测试和代码分支。我知道有很多模拟库声称可以模拟,但我觉得我最大的问题是代码设计本身。
比方说,我会在任何面向对象语言(示例中为 java)中编写相同的代码,我可以编写一些接口(interface),并在测试中简单地覆盖我想要模拟的默认行为,设置一个假的 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 documentation ,有一个与我在java上给出的类似的例子。一些答案指向特征,clojures , conditional compiliation
我们可能会在测试中提供一些场景,第一个是一些 mod.rs 中的公共(public)函数
#[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
绑定(bind)在一起。 ,当然,这可能会扩展到任何非确定性函数,如时间、数据库连接等。您将如何设计此代码或任何类似代码以更容易进行单元测试和更好的设计?
最佳答案
How would you design this or any similar code to make easier to unit testing and better design?
一种方法是制作
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))
.至于特征示例,我认为您误解了如何将模式应用于您的代码。您应该使用 trait 将获取数据与处理数据分离,就像您在 Java 中使用接口(interface)的方式一样。
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!(...);
}
}
关于unit-testing - rust 中的单元测试、模拟和特征,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/63850791/