Rust模拟另一个结构实现消耗的特征

时间:2020-04-02 17:41:47

标签: unit-testing rust

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

我创建了一个特征,其中封装了PathBuf.is_dir方法,根据mockall documentation,理论上,该方法应该可以模拟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 个答案:

答案 0 :(得分:2)

不幸的是,至少在您想要的方式下,您正在做的事情行不通。 #[automock]正在创建一个名为struct的{​​{1}}。然后,您可以将该类型传递到期望实现MockPathInterface的函数中。它至少不能直接改变实现该特征的现有结构的行为。您确实在测试中正确设置了PathInterface,但是没有将其与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

这里唯一的变化是使用turbfish(#[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); } )运算符将类型传递给::<>静态方法。

这些更改在美学上很难看,我并不一定建议您在所有地方都使用此模式-如果您确实必须嘲笑很多这样的行为,则基本上是无法维护的。但这说明了如何实现,以及Rust中模拟的局限性。

答案 1 :(得分:0)

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