如何使外部板条箱(或其周围的方法)生锈



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

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板条箱),使用相同的功能和什么。

我尝试创建一个性状和扩展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)
}
/// ..............
/// ..............
/// ..............
}


所以,首先,确实有模拟目标的辅助工具和箱子,如::mockall,以帮助与这些模式,,但只有当你已经有一个基于特征的API。如果你不这样做,那部分会很乏味。

值得注意的是,要知道还有其他帮助箱可以帮助编写锅炉板y和冗余委托的特性暗示,例如您的open -> open情况。::delegate板条箱就是这样一个例子。


用一个测试目标Cargo特性模拟它

说了这么多,我个人对您非常特殊的情况的看法-目标是用模拟的覆盖真正的impl,但仅用于测试目的-,将是放弃结构化但重量级的泛型方法&特征,而不是拥抱"鸭子型";api,就像在不同平台上实现时经常做的那样。换句话说,以下建议在概念上可以解释为您的测试环境是这样一个特殊的"平台"。

然后,您将#[cfg(…)]-feature-gate使用真正的impl,即CANSocket类型,在一种情况下,#[cfg(not(…))]-feature-gate使用您自己的CANSocket类型的模拟定义,前提是您能够复制/模拟您自己可能正在使用的所有真正的类型API。

  1. 为您的项目添加mock-socketCargo功能:

    [features]
    mock-socket = []
    
    • 备注:你们中的一些人可能正在考虑使用cfg(test)而不是cfg(feature = "…"),但这种方法只适用于单元(src/…文件与#[cfg(test)] mod tests,cargo test --lib调用)测试,它不用于集成测试(tests/….rs文件,cargo test --tests调用)或doctests (cargo test --doc调用),因为库本身然后编译而没有cfg(test)
  2. 然后你可以使用它来对Rust代码进行功能限制

    #[cfg(not(feature = "mock-socket"))]
    use …path::to::genuine::CANSocket;
    #[cfg(feature("mock-socket"))]
    use my_own_mock_socket::CANSocket;
    
  3. 这样你就可以定义my_own_mock_socket模块(),在使用mod my_own_mock_socket;声明的my_own_mock_socket.rs文件中),只要您不忘记对其本身进行特性门,以便编译器在不使用模拟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 */
    }
    …
    }
    }
    
  4. 这样,您可以使用:

    • 任意cargo test
    • cargo test --features mock-socket

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

  5. (可选)如果您知道永远不想为真实实现运行测试,而只想为模拟实现运行测试,那么您可能希望在运行测试时默认启用该特性。虽然没有直接的方法可以实现这一点,但有一种创造性的方法可以解决这个问题,通过显式地告诉测试代码具有的自定义库开发依赖关系(这种依赖关系总是隐式地存在,无论如何)。通过使其显式,我们可以使用经典的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 imps足够短时,只内联它的定义和impl块可能更有吸引力。然后的问题是,对于这样定义的每个项目,它必须携带#[cfg…]属性,这很烦人。这时,像https://docs.rs/cfg-if这样的helper宏就可以发挥作用了,尽管为这样一个简单的宏添加依赖关系似乎有点过分(而且,非常个人)。(我发现cfg_if!的语法符号太重了)。

相反,你可以自己用不到十几行代码重新实现它:

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;
},
}

通过使用宏来创建包装器特性并为基结构体实现它,可以避免大量的样板文件。简化的例子:

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) -> ();
}
}

游乐场

需要扩展以处理引用(至少是&self参数),但是您可以理解。关于编写宏的更多信息,可以参考《Rust宏小书》。

然后您可以使用像mockall这样的crate来创建您的TestTrait的模拟实现,或者滚动您自己的。

相关内容

最新更新