如何在Rust中模拟外部依赖

huangapple go评论77阅读模式
英文:

How do I mock an external dependency in Rust

问题

我有一个调用外部依赖walkdir::WalkDir的方法,并希望测试我是否正确处理了它的输出。理想情况下,通过模拟依赖的响应来进行测试。

我的代码如下:

use walkdir::{IntoIter, WalkDir};

pub struct MyStruct<'a> {
    pub path: &'a Path
}

impl MyStruct {
    fn _walk_wrapper(&self) -> IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    pub fn foo(&self) -> Vec<&str> {
        let walk = self._walk_wrapper();

        // 对遍历结果进行处理
        // 返回一个值的向量
    }
}

任何帮助都会受到欢迎。

英文:

I have a method which calls the external dependency walkdir::WalkDir, and would like to test that I'm handling its output correctly. Ideally by mocking the response of the dependency.

My code looks like this:

use walkdir::{IntoIter,WalkDir};

pub struct MyStruct&lt;&#39;a&gt; {
    pub path: &amp;&#39;a Path
}

impl MyStruct {
    fn _walk_wrapper(&amp;self) -&gt; IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    pub fn foo(&amp;self) -&gt; Vec&lt;&amp;str&gt; {
        let walk = self._walk_wrapper();

        // do something with the walk results
        // return a vector of values
    }
}

Any help is appreciated

答案1

得分: 2

  1. 只有有条件地实现这个方法不同。
use walkdir::{IntoIter, WalkDir};

pub struct MyStruct<'a> {
    pub path: &'a Path
}

impl MyStruct {
    #[cfg(not(test))]
    fn _walk_wrapper(&self) -> impl IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    #[cfg(test)]
    fn _walk_wrapper(&self) -> impl IntoIter {
        ["x.txt", "my_cat.jps"].into_iter()
    }

    pub fn foo(&self) -> Vec<&str> {
        let walk = self._walk_wrapper();

        // 处理遍历结果
        // 返回值向量
    }
}

这种方法有很大的缺点,例如,所有的测试都会得到相同的输出。你将无法在未来测试真正的行为。

你可以尝试使用 mockall 进行测试的模拟。

  1. 使用泛型和特性:
use walkdir::{IntoIter, WalkDir};

trait WalkDirImpl {
   type It: Iterator;
   fn get_values(path: &Path) -> Self::It;
}

impl WalkDirImpl for WalkDir {
   type It: ...;
   fn get_values() -> Self::It{
        WalkDir::new(self.path)
            .into_iter()
    }
}

pub struct MyStruct<'a> {
    pub path: &'a Path
}

impl MyStruct {
    #[cfg(not(test))]
    fn _walk_wrapper(&self) -> impl IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    #[cfg(test)]
    fn _walk_wrapper(&self) -> impl IntoIter {
        ["x.txt", "my_cat.jps"].into_iter()
    }

    pub fn foo<WalkWrapper = WalkDir>(&self) -> Vec<&str>
       where WalkWrapper: WalkDirImpl
    {
        let walk = self._walk_wrapper();

        // 处理遍历结果
        // 返回值向量
    }
}

#[test]
fn my_test() {
    struct TestProvider;
    impl WalkDirImpl for TestProvider{
       type It: ...;
       fn get_values() -> Self::It{
          ["x.txt", "my_cat.jps"].into_iter()
       }
    }
    
    MyStruct::foo::<TestProvider>();
}

这种方法也有缺点。例如,你开始从公共接口中泄露外部类型。为了防止这种情况,你可以将通用的结构/方法封装到一些公共的结构/方法中,并使用内部的私有通用结构/方法。

  1. 这是第一种方法的变种。
type OverrideFun = fn() -> Vec<PathBuf>;
// 使用线程局部变量来存储覆盖函数。
// 线程局部变量是因为测试并行运行。
#[cfg(test)]
thread_local!{
   static OVERRIDE_FUN_PTR: Cell<Option<OverrideFun>> = Cell::new(None);
};

fn get_children(path: &Path) -> Vec<PathBuf>{
   // 如果是测试,尝试加载覆盖函数。
   #[cfg(test)]
   {
      let mut res = None;
      OVERRIDE_FUN_PTR.with(|cell|{
         let fun = cell.get();
         res = fun();
      });
      if let Some(res) = res{
         return res;
      }
   }

   WalkDir::new(path)
            .into_iter()
            .collect()
}

#[test]
fn my_test(){
   OVERRIDE_FUN_PTR.with(|cell|{
         cell.set(||vec!["x.txt", "my_cat.jpg"]);
   });
   assert_eq!(get_children(""), vec!["x.txt", "my_cat.jpg"]);
}

这个版本很好,因为它是可配置的。由于 #[cfg(test)],它只影响测试,不会编译到最终的二进制文件中。你可以在线程局部变量中存储任何东西。

英文:

3 approaches:

  1. Just conditionally implement this method differently.
use walkdir::{IntoIter,WalkDir};

pub struct MyStruct&lt;&#39;a&gt; {
    pub path: &amp;&#39;a Path
}

impl MyStruct {
    #[cfg(not(test))]
    fn _walk_wrapper(&amp;self) -&gt; impl IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    #[cfg(test)]
    fn _walk_wrapper(&amp;self) -&gt; impl IntoIter {
        [&quot;x.txt&quot;, &quot;my_cat.jps&quot;].into_iter()
    }

    pub fn foo(&amp;self) -&gt; Vec&lt;&amp;str&gt; {
        let walk = self._walk_wrapper();

        // do something with the walk results
        // return a vector of values
    }
}

This approach has big downsides, for example, all your tests would get same output.
And you wouldn't be able test real behaviour in future.

You may try to use mockall for mocking tests.

  1. Use generics and traits:
use walkdir::{IntoIter,WalkDir};

trait WalkDirImpl {
   type It: Iterator;
   fn get_values(path: &amp;Path)-&gt;Self::It;
}

impl WalkDirImpl for WalkDir {
   type It: ...;
   fn get_values() -&gt; Self::It{
        WalkDir::new(self.path)
            .into_iter()
    }
}

pub struct MyStruct&lt;&#39;a&gt; {
    pub path: &amp;&#39;a Path
}

impl MyStruct {
    #[cfg(not(test))]
    fn _walk_wrapper(&amp;self) -&gt; impl IntoIter {
        WalkDir::new(self.path)
            .into_iter()
    }

    #[cfg(test)]
    fn _walk_wrapper(&amp;self) -&gt; impl IntoIter {
        [&quot;x.txt&quot;, &quot;my_cat.jps&quot;].into_iter()
    }

    pub fn foo&lt;WalkWrapper=WalkDir&gt;(&amp;self) -&gt; Vec&lt;&amp;str&gt;
       where WalkWrapper: WalkDirImpl
    {
        let walk = self._walk_wrapper();

        // do something with the walk results
        // return a vector of values
    }
}
#[test]
fn my_test() {
    struct TestProvider;
    impl WalkDirImpl for TestProvider{
       type It: ...;
       fn get_values() -&gt; Self::It{
          [&quot;x.txt&quot;, &quot;my_cat.jps&quot;].into_iter()
       }
    }
    
    MyStruct::foo::&lt;TestProvider&gt;
}

This approach has downsides too. For example, you start to leak foreign type from your public interface.
To prevent this, you can wrap your generic struct/methdod into some public struct/method and use inner private generic struct/method.

  1. This is variation of first approach.
type OverrideFun = fn()-&gt;Vec&lt;PathBuf&gt;;
// Use thread local to store override function.
// Thread local because tests runs in parallel.
#[cfg(test)]
thread_local!{
   static OVERRIDE_FUN_PTR: Cell&lt;Option&lt;OverrideFun&gt;&gt; = Cell::new(None);
};

fn get_children(path:&amp;Path)-&gt;Vec&lt;PathBuf&gt;{
   // If test, try to load override.
   #[cfg(test)]
   {
      let mut res = None;
      OVERRIDE_FUN_PTR.with(|cell|{
         let fun = cell.get();
         res = fun();
      });
      if let Some(res) = res{
         return res;
      }
   }


   WalkDir::new(path)
            .into_iter()
            .collect()
}

#[test]
fn my_test(){
   OVERRIDE_FUN_PTR.with(|cell|{
         cell.set(||vec![&quot;x.txt&quot;, &quot;my_cat.jpg&quot;]);
   });
   assert_eq!(get_children(&quot;&quot;), vec![&quot;x.txt&quot;, &quot;my_cat.jpg&quot;]);
}

This version is good because it is configurable. Because #[cfg(test)] it only affects tests and wouldn't be compiled into your result binary.
And you could store anything in that threadlocal, really.

huangapple
  • 本文由 发表于 2023年5月14日 02:11:22
  • 转载请务必保留本文链接:https://go.coder-hub.com/76244240.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定