The rtnetlink crate provides an interface to the Linux netlink functionality for defining network interfaces, routes, etc. I would like to use this crate as part of a program and I'd also like to unit test my interactions with the crate. However, after reading through the documentation for mockall
and double
, it's not clear how I could mock the interactions with this dependency (or if it's possible at all).
The first interaction with the dependency is to to call rtnetlink::new_connection
. As far as I can tell, the mockall
crate can't mock a bare function from another crate. But by using double
I can wrap the call to new_connection
with a pair of methods in my crate - one of which calls the real new_connection
and one of which is a hand-written mock. In the mock'd module, I can define my own Traits/Structs which mirror the real ones.
// Module to use for indirection for mocks
pub mod dependencies {
// Module holding the real implementations of the renetlink crate
// Publicly re-export all the needed types, so that the rest of the program uses
// dependencies::net::Handle, which can then be conditionally compiled by double
pub mod net {
pub use rtnetlink:Handle
pub use ....
// Wrap the unmockable function into a function with the same type signature.
pub fn init() -> Result<(Connection<..>, Handle, UnboundedReceiver<...>)> {
return rtnetlink::new_connection();
}
}
// Module to hold mock implementations
pub mod mock_net {
// Instead of publicly re-exporting a real Handle, use a stub Handle
pub struct Handle;
pub ....
// Return a mock from the wrapper function instead of the real implementation
pub fn init() -> Result<(Connection<..>, Handle, UnboundedReceiver<...>)> {
let (_, receiver) = futures_channel::mpsc::unbounded();
Ok((Connection {} , Handle {}, receiver))
}
}
}
// Conditionally compile the use statement.
// Real code uses external::net
// Test code uses external::mock_net
#[double]
use crate::external::net;
So that gets me past my first interaction. In unit tests, I can now call init()
and get back stubs/mocks instead of the real implementation
#[cfg(test)]
mod tests {
use super::net;
#[tokio::test]
async fn can_i_mock_it() {
let (connection, handle, _) = net::init().unwrap();
}
}
So now I need to solve my next challenge. Once I have a Handle
, the next thing the program needs to do is interact with it. So now I need to mock out Handle
, which is a struct in another crate. The mockall
docs explain how to mock a struct in your own crate, but not an external dependency struct.
let (connection, handle, _) = net::init()....?;
tokio::spawn(connection);
// I'm not going to hand-write .address(), nor .add(), etc.
handle.address().add(1, IpAddr::V4(Ipv4Addr::from(1)), 1);
Attempting to hand-implement all the used methods on Handle
(and every other struct from rtnetlink
that I use would be a massive effort. I was hoping some macro could produce an implementation of Handle
in the mock_net
module that had the same interfaces as Handle
from my dependency.