I'm trying to test a struct I have that looks something like this
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 }
}
}
What I want to test is that the proper messages are being sent across the socket, but I don't want to actually initialize a new can device while running my tests. I wanted to make a dummy CANSocket (which is from the cansocket crate) that uses the same functions and whatnot.
I tried creating a trait and extending the socketcan::CANSocket
but it is super tedious and very redundant. I've looked at the mockall
crate but I'm not sure if this would help in this situation. Is there an elegant way to accomplish what I want?
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)
}
/// ..............
/// ..............
/// ..............
}
CodePudding user response:
So, first of all, there are indeed mock-targeted helper tools and crates such as ::mockall
to help with these patterns, but only when you already have a trait-based API. If you don't, that part can be quite tedious.
For what is worth, know that there are also other helper crates to help write that boiler-plate-y and redundantly-delegating trait impls such as your open -> open
situation. One such example could be the ::delegate
crate.
Mocking it with a test-target Cargo feature
With all that being said, my personal take for your very specific situation —the objective is to override a genuine impl with a mock one, but just for testing purposes—, would be to forgo the structured but heavyweight approach of generics & traits, and to instead embrace "duck-typed" APIs, much like it is often done when having implementations on different platforms. In other words, the following suggestion, conceptually, could be interpreted as your test environment being one such special "platform".
You'd then #[cfg(…)]
-feature-gate the usage of the real impl, that is, the CANSocket
type, in one case, and #[cfg(not(…))]
-feature gate a mock definition of your own CANSocket
type, provided you managed to copy / mock all of the genuine's type API that you may, yourself, be using.
Add a
mock-socket
Cargo feature to your project:[features] mock-socket = []
- Remark: some of you may be thinking of using
cfg(test)
rather thancfg(feature = "…")
, but that approach only works for unit (src/…
files with#[cfg(test)] mod tests
,cargo test --lib
invocation) tests, it doesn't for integration tests (tests/….rs
files,cargo test --tests
invocation) or doctests (cargo test --doc
invocation), since the library itself is then compiled withoutcfg(test)
.
- Remark: some of you may be thinking of using
Then you can feature-gate Rust code using it
#[cfg(not(feature = "mock-socket"))] use …path::to::genuine::CANSocket; #[cfg(feature("mock-socket"))] use my_own_mock_socket::CANSocket;
So that you can then define that
my_own_mock_socket
module (e.g., in amy_own_mock_socket.rs
file usingmod my_own_mock_socket;
declaration), provided you don't forget to feature-gate it itself, so that the compiler doesn't waste time and effort compiling it when not using the mockedCANSocket
(which would yielddead_code
warnings and so on):#[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 */ } … } }
That way, you can use:
- either
cargo test
- or
cargo test --features mock-socket
to run pick the implementation of your choice when running your tests
- either
(Optional) if you know you will never want to run the tests for the real implementation, and only the mock one, then you may want to have that feature be enabled by default when running tests. While there is no direct way to achieve this, there is a creative way to work around it, by explicitly telling of the self-as-a-lib dev-dependency that test code has (this dependency is always present implicitly, for what is worth). By making it explicit, we can then use the classic
features
.toml
attribute to enable features for thatdev-dependency
:[dev-dependencies] your_crate_name = { path = ".", features = ["mock-socket"] }
Bonus: not having to define an extra module for the mock code.
When the mock impls in question are short enough, it could be more tempting to just inline its definition and impl
blocks. The issue then is that for every item so defined, it has to carry that #[cfg…]
attribute which is annoying. That's when helper macros such as that of https://docs.rs/cfg-if can be useful, albeit adding a dependency for such a simple macro may seem a bit overkill (and, very personally, I find cfg_if!
's syntax too sigil heavy).
You can, instead, reimplement it yourself in less than a dozen lines of code:
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;
With it, you can rewrite steps 2.
and 3.
above as:
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;
},
}
CodePudding user response:
You can avoid a lot of the boilerplate by using a macro to create the wrapper trait and implement it for the base struct. Simplified example:
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) -> ();
}
}
This will need to be extended to handle references (at least &self
arguments), but you get the idea. You can refer to The Little Book of Rust Macros for more information on writing the macro.
Then you can use a crate like mockall
to create your mock implementation of TestTrait
or roll your own.