如何在外部板条箱(或其周围的方式)上生 rust

kx7yvsdv  于 2023-05-17  发布在  其他
关注(0)|答案(2)|浏览(154)

我正在测试一个结构体,它看起来像这样

struct CANProxy {
  socket: CANSocket
  // other stuff .......
}
impl CANProxy {
  pub fn new(can_device: &str) -> Self {
    let socket = CANSocket::open(can_device).unwrap();
    // other stuff .......
    
    Self { socket }
  }
}

我想测试的是通过套接字发送的消息是否正确,但我不想在运行测试时实际初始化新的can设备。我想做一个虚拟的CANSocket(来自cansocket crate),它使用相同的函数和诸如此类的东西。
我试着创建一个trait并扩展socketcan::CANSocket,但它非常繁琐和冗余。我已经看过mockall机箱,但我不确定这是否会在这种情况下有所帮助。有没有一种优雅的方式来实现我想要的?

trait CANInterface {
  fn open(name: &str) -> Result<Self, SomeError>;
  // ... all the functions that are a part of the socketcan::CANSocket
  // which is a lot of repetition
}

///////////// Proxy code
struct<T: CANInterface> CANProxy<T> {
  socket: T
  // other stuff .......
}
impl<T: CANInterface> CANProxy<T> {
  pub fn open(can_device: &str) -> Result<Self, SomeError> {
    let socket = T::open(can_device).unwrap();
    // other stuff .......
    
    Ok(Self { socket })
  }
}

////////////// Stubbed CANInterfaces
struct FakeCANSocket;
impl CANInterface for FakeCANSocket {
  // ..... implementing the trait here
}
// extension trait over here
impl CANInterface for socketcan::CANSocket {
  // this is a lot of repetition and is kind of silly
  // because I'm just calling the same things
  fn open(can_device: &str) -> Self {
    CANSocket::open(can_device)
  }
  /// ..............
  /// ..............
  /// ..............
}
gopyfrb3

gopyfrb31#

因此,首先,确实有一些针对mock的辅助工具和crate(如::mockall)来帮助处理这些模式,* 但前提是您已经有了基于trait的API*。如果你不这样做,那部分可能会很乏味。
值得一提的是,还有其他的helper crates来帮助编写样板式的y和冗余委派的trait impls,比如你的open -> open情况。一个这样的例子可能是::delegate机箱。

使用测试目标Cargo功能进行模拟

话虽如此,我个人对你的特殊情况的看法-目标是用一个mock来覆盖一个真正的impl,但只是为了测试目的-将放弃泛型和traits的结构化但重量级的方法,而是拥抱“鸭子类型”的API,* 就像在不同平台上实现时经常做的那样。换句话说,下面的建议,从概念上讲,可以解释为您的测试环境是一个这样的特殊“平台”。
然后,您可以使用#[cfg(…)]-feature-gate来使用真实的的impl,即在一种情况下使用CANSocket类型,并使用#[cfg(not(…))]-feature gate来模拟您自己的CANSocket类型的定义,前提是您设法复制/模拟了您自己可能正在使用的所有真正的类型API。

  • mock-socket Cargo功能添加到您的项目中:
[features]
mock-socket = []
  • 备注:有些人可能会考虑使用cfg(test)而不是cfg(feature = "…"),但这种方法只适用于 unit(带有#[cfg(test)] mod testscargo test --lib调用的src/…文件)测试,它不用于集成测试(tests/….rs文件,cargo test --tests调用)或doctests(cargo test --doc调用),因为库本身然后在没有 * cfg(test)的情况下编译。
  • 然后你就可以使用它对Rust代码进行功能门控
#[cfg(not(feature = "mock-socket"))]
use …path::to::genuine::CANSocket;

#[cfg(feature("mock-socket"))]
use my_own_mock_socket::CANSocket;
  • 这样你就可以定义这个my_own_mock_socket模块(* 例如 *,在一个my_own_mock_socket.rs文件中使用mod my_own_mock_socket;声明),前提是你不要忘记对它本身进行功能门控,这样编译器在不使用模拟的CANSocket时就不会浪费时间和精力编译它(这会产生dead_code警告等):
#[cfg(feature = "mock-socket")]
mod my_own_mock_socket {
    //! It is important that you mimic the names and APIs of the genuine type!
    pub struct CANSocket…

    impl CANSocket { // <- no traits!
        pub fn open(can_device: &str) -> Result<Self, SomeError> {
            /* your mock logic */
        }

        …
    }
}
  • 这样,您可以用途:
  • cargo test
  • cargo test --features mock-socket

在运行测试时选择所需的实现

  • (可选)如果您知道您永远不想运行真实的实现的测试,而只想运行模拟实现的测试,那么您可能希望在运行测试时默认启用该功能。虽然没有直接的方法来实现这一点,但有一种创造性的方法可以解决这个问题,通过显式地告诉测试代码具有的self-as-a-lib dev-dependency(这种依赖总是隐式地存在,无论如何)。通过使其显式化,我们可以使用经典的features.toml属性来启用该dev-dependency的功能:
[dev-dependencies]
your_crate_name.path = "."  # <- this is always implicitly present
your_crate_name.features = ["mock-socket"]  # <- we add this
奖励:而不必为模拟代码定义额外的模块。

当所讨论的mock impls足够短时,将其定义和impl块内联可能更有吸引力。问题是,对于每个这样定义的项目,它必须携带#[cfg…]属性,这很烦人。这时,像https://docs.rs/cfg-if这样的辅助宏就很有用了,尽管为这样一个简单的宏添加一个依赖似乎有点大材小用(而且,非常个人,我发现cfg_if!的语法太符号化了)。
相反,你可以用不到12行代码自己重新实现它:

macro_rules! cfg_match {
    ( _ => { $($tt:tt)* } $(,)? ) => ( $($tt)* );
    ( $cfg:meta => $expansion:tt $(, $($($rest:tt)+)?)? ) => (
        #[cfg($cfg)]
        cfg_match! { _ => $expansion }
        $($(
            #[cfg(not($cfg))]
            cfg_match! { $($rest)+ }
        )?)?
    );
} use cfg_match;

使用它,您可以将上面的步骤2.3.重写为:

cfg_match! {
    feature = "mock-socket" => {
        /// Mock implementation
        struct CANSocket …

        impl CANSocket { // <- no traits!
            pub fn open(can_device: &str) -> Result<Self, SomeError> {
                /* your mock logic */
            }
            …
        }   
    },
    _ => {
        use …path::to::genuine::CANSocket;
    },
}
gfttwv5a

gfttwv5a2#

您可以通过使用宏创建 Package 器trait并为基本结构实现它来避免很多样板。简化示例:

macro_rules! make_wrapper {
    ($s:ty : $t:ident { $(fn $f:ident ($($p:ident $(: $pt:ty)?),*) -> $r:ty;)* }) => {
        trait $t {
            $(fn $f ($($p $(: $pt)?),*) -> $r;)*
        }
        impl $t for $s {
            $(fn $f ($($p $(: $pt)?),*) -> $r { <$s>::$f ($($p),*) })*
        }
    }
}

struct TestStruct {}

impl TestStruct {
    fn foo (self) {}
}

make_wrapper!{
    TestStruct: TestTrait {
        fn foo (self) -> ();
    }
}

Playground
这将需要扩展以处理引用(至少&self参数),但您可以理解。您可以参考The Little Book of Rust Macros以获得有关编写宏的更多信息。
然后,您可以使用像mockall这样的crate来创建TestTrait的模拟实现,或者自己创建。

相关问题