Rust如何对被另一个结构体实现消耗的trait进行mocking

3

我严格地尝试模仿std::path::PathBuf.is_dir方法,但我认为这里有一个更通用的用例,实际上这是关于模拟外部功能。

我创建了一个trait来封装PathBuf.is_dir方法,理论上,根据mockall文档,应该能够使我可以模拟我的is_dir封装.

use mockall::*;
use std::path::PathBuf;

#[derive(Debug, Clone, PartialEq)]
pub enum PackageFileIndexError {
    ArchiveRootNotADirectory,
}

#[automock]
trait PathInterface {
    // Encapsulate the is_dir method to make it mockable.
    fn is_dir(this_path: &PathBuf) -> bool {
        this_path.is_dir()
    }
}

pub struct PackageFileIndexData {
    archive_root_path: PathBuf,
}

impl PackageFileIndexData {
    pub fn new(archive_root: &str) -> Result<PackageFileIndexData, PackageFileIndexError> {
        let archive_root_path = PathBuf::from(archive_root.clone());

        if !Self::is_dir(&archive_root_path) {
            return Err(PackageFileIndexError::ArchiveRootNotADirectory);
        }

        Ok(PackageFileIndexData { archive_root_path })
    }
}

impl PathInterface for PackageFileIndexData {}

#[cfg(test)]
mod tests {
    use super::*;

    mock! {
        PackageFileIndexData {}

        trait PathInterface {
            fn is_dir(this_path: &PathBuf) -> bool;
        }
    }

    #[test]
    fn test_bad_directory() {
        let ctx = MockPackageFileIndexData::is_dir_context();
        ctx.expect().times(1).returning(|_x| false);

        let result = PackageFileIndexData::new("bad_directory").err().unwrap();

        assert_eq!(result, PackageFileIndexError::ArchiveRootNotADirectory);
    }

    #[test]
    fn test_good_directory() {
        let ctx = MockPackageFileIndexData::is_dir_context();
        ctx.expect().times(1).returning(|_x| true);

        let _result = PackageFileIndexData::new("good_directory").unwrap();
    }

    #[test]
    fn test_bad_directory2() {
        let ctx = MockPathInterface::is_dir_context();
        ctx.expect().times(1).returning(|_x| false);

        let result = PackageFileIndexData::new("bad_directory").err().unwrap();

        assert_eq!(result, PackageFileIndexError::ArchiveRootNotADirectory);
    }

    #[test]
    fn test_good_directory2() {
        let ctx = MockPathInterface::is_dir_context();
        ctx.expect().times(1).returning(|_x| true);

        let _result = PackageFileIndexData::new("good_directory").unwrap();
    }
}

所有这些测试都失败了,原因如下。我认为可用的模拟对象(测试发现了各种模拟上下文)未被正在运行的测试使用。
---- mock_is_dir::tests::test_good_directory1 stdout ----
thread 'mock_is_dir::tests::test_good_directory1' panicked at 'called `Result::unwrap()` on an `Err` value: ArchiveRootNotADirectory', src/mock_is_dir.rs:63:23

---- mock_is_dir::tests::test_bad_directory2 stdout ----
thread 'mock_is_dir::tests::test_bad_directory2' panicked at 'MockPathInterface::is_dir: Expectation(<anything>) called fewer than 1 times', src/mock_is_dir.rs:10:1

---- mock_is_dir::tests::test_good_directory2 stdout ----
thread 'mock_is_dir::tests::test_good_directory2' panicked at 'called `Result::unwrap()` on an `Err` value: ArchiveRootNotADirectory', src/mock_is_dir.rs:81:23

---- mock_is_dir::tests::test_bad_directory1 stdout ----
thread 'mock_is_dir::tests::test_bad_directory1' panicked at 'MockPackageFileIndexData::is_dir: Expectation(<anything>) called fewer than 1 times', src/mock_is_dir.rs:40:5
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace


failures:
    mock_is_dir::tests::test_bad_directory1
    mock_is_dir::tests::test_bad_directory2
    mock_is_dir::tests::test_good_directory1
    mock_is_dir::tests::test_good_directory2

test result: FAILED. 0 passed; 4 failed; 0 ignored; 0 measured; 0 filtered out
2个回答

3
很不幸,你所做的事情不会按照你想要的方式起作用。 #[automock] 创建了一个名为MockPathInterfacestruct。您可以将该类型传递给期望实现PathInterface的函数。它不能直接改变已实现该trait的现有结构体的行为。在测试中,您确实正确地设置了MockPathInterface,但没有任何关联它与PackageFileInfoData,因此它从未被使用。
一种方法是修改代码以传递行为,然后将您模拟的行为传递进去。例如:
#[automock]
pub trait PathInterface {
    // Encapsulate the is_dir method to make it mockable.
    fn is_dir(this_path: &PathBuf) -> bool {
        this_path.is_dir()
    }
}

pub struct PackageFileIndexData {
    archive_root_path: PathBuf,
}

impl PackageFileIndexData {
    pub fn new<PI: PathInterface>(archive_root: &str) -> Result<PackageFileIndexData, PackageFileIndexError> {
        let archive_root_path = PathBuf::from(archive_root.clone());

        if !PI::is_dir(&archive_root_path) {
            return Err(PackageFileIndexError::ArchiveRootNotADirectory);
        }

        Ok(PackageFileIndexData { archive_root_path })
    }
}

请注意,我们在new静态方法中使用通用参数来传递实现该特质的类型。还要注意,我们更改了is_dir调用以引用该通用类型。
然后,您可以修改测试以使用该类型参数(下面的示例是其中之一):
#[test]
fn test_bad_directory() {
    let ctx = MockPathInterface::is_dir_context();
    ctx.expect().times(1).returning(|_x| false);

    let result = PackageFileIndexData::new::<MockPathInterface>("bad_directory").err().unwrap();

    assert_eq!(result, PackageFileIndexError::ArchiveRootNotADirectory);
}

这里唯一的变化是使用了一个Turbofish (::<>) 运算符将类型传递给 new 静态方法。

这些变化在美学上有些不太好看,我并不一定建议你在每个地方都使用这种模式,如果你真的必须像这样模拟很多行为,那么它基本上是不可维护的。但这说明了如何做到这一点以及在Rust中模拟的限制。


我需要考虑一下这种方法是否更易于维护,但我猜想除了使用通用构造函数建议之外的另一种选择是使用 cfg-if 或类似的宏级编译时控制。我认为通用构造函数比使用宏更简洁,但可维护性让我担忧。如果在这里不能或不应该使用模拟,那么测试这种实现的惯用 Rust 方法是什么? - blueskyjunkie
使用cfg-if来模拟结构体是一个非常好的选择,虽然我之前没有使用过cfg-if。在mockall文档中有使用它来mock结构体的示例。我认为两种方法都不是“错”的,尽管mocking结构体似乎更像hacky。除非没有其他选择,否则我不喜欢mock,但在必要的情况下,我会使用通用构造器风格的方法。 - Richard Matheson

0

使用cfg-if正是Mockall旨在模拟具体结构体时所要使用的方式。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接