如何模拟 `std::fs::File` 以便检查 `File::set_len` 在单元测试中是否正确使用?

Grz*_*cki 5 unit-testing mocking rust

我检查,File::set_len(..)它似乎是为 实现的struct File,但不是通过 Trait 实现的。

目标:测试foo以读/写方式打开文件,执行以下操作:读取、写入、查找和将文件修剪到一定大小。我们喜欢在测试中提供文件的初始状态,并检查结果。最好是在内存中。

如何测试依赖的代码set_len?(io::Seek或者其他特征到目前为止没有帮助)。

我想嘲笑它。

让我们做一个玩具示例,以使讨论更容易:

#![allow(unused_variables)]

use std::error::Error;
use std::fs::File;
use std::io::Cursor;

// assumes that file is open in Read/Write mode
// foo performs reads and writes and Seeks
// at the end wants to trim size of file to certain size.
fn foo(f: &mut File) -> Result<(), Box<dyn Error>> {
    f.set_len(0)?;
    Ok(())
}

fn main () -> Result<(), Box<dyn Error>> {
    let mut buf = Vec::new();
    let mut mockfile = Cursor::new(&buf);
    // we would like to supply foo
    // with "test" representation of initial file state
    foo(&mut mockfile)
    // and check afterwards if resulting contents (=> size)
    // of file match expectations
}
Run Code Online (Sandbox Code Playgroud)

关于 rust-play :https://play.rust-lang.org/? version=stable&mode=debug&edition=2018&gist=950a94504168d51f043966288fae3bca

错误:

error[E0308]: mismatched types
  --> src/main.rs:15:9
   |
15 |     foo(&mut mockfile)
   |         ^^^^^^^^^^^^^ expected struct `File`, found struct `std::io::Cursor`
Run Code Online (Sandbox Code Playgroud)

PS在收到答案之前,我开始尝试创建箱子tempfile: https: //docs.rs/tempfile/3.1.0/tempfile/#structs。尽管如此,理想的解决方案是“内存中”,所以迫不及待地等待问题的答案:)。

use*_*342 6

简而言之,你不能模拟std::fs::File一个需要精确类型的函数——这不是 Rust 的工作方式。

然而,如果你可以控制foo,你可以很容易地发明一个具有该特征的特征set_len并使foo该特征变得通用。由于这是您的特征,因此您可以为其他地方定义的类型(例如File)实现它,这将像以前一样foo()接受。File但它也接受任何其他实现该特征的东西,包括您在测试套件中创建的模拟类型。由于单态化,它的执行将与原始代码一样高效。例如:

pub trait SetLen {
    fn set_len(&mut self, len: u64) -> io::Result<()>;
}

impl SetLen for File {
    fn set_len(&mut self, len: u64) -> io::Result<()> {
        File::set_len(self, len)
    }
}

pub fn foo(f: &mut impl SetLen) -> Result<(), Box<dyn Error>> {
    f.set_len(0)?;
    Ok(())
}

// You can always pass a `File` to `foo()`:
fn main() -> Result<(), Box<dyn Error>> {
    let mut f = File::create("bla")?;
    foo(&mut f)?;
    Ok(())
}
Run Code Online (Sandbox Code Playgroud)

要模拟它,您只需定义一个实现该特征的类型并记录它是否被调用:

#[derive(Debug, Default)]
struct MockFile {
    set_len_called: Option<u64>,
}

impl SetLen for MockFile {
    fn set_len(&mut self, len: u64) -> io::Result<()> {
        self.set_len_called = Some(len);
        Ok(())
    }
}

#[test]
fn test_set_len_called() {
    let mut mf = MockFile::default();
    foo(&mut mf).unwrap();
    assert_eq!(mf.set_len_called, Some(0));
}
Run Code Online (Sandbox Code Playgroud)

操场

  • @GrzegorzWierzowiecki 特定于测试的包装器并不总是必要的,许多函数由于其他原因是通用的,并且很容易模拟它们。(例如,无论如何,一个函数可能需要与其他类似文件的对象一起工作。)也许 Rust 开发人员根本不热衷于用更动态的语言模拟框架所鼓励的那种微观单元测试。 (3认同)