Is it possible to mock this crate for unit testing?

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.

I think I have figured it out, but I'd be happy to know that I found the ugly way to do it and that there's a much nicer way.

pub mod dependencies {
  // ....
  pub mod mock_net {
    // ...
    mock! {
      pub Handle {
        pub fn address(&self) -> AddressHandle;
      }
    }
    pub type Handle = MockHandle;
    // ..
  }
}

When I first read the mock! documentation , I thought it was required to have an impl block. When I tried that, I got procedural macro error saying that structs had to implement a Trait, so I thought that mock! just couldn't be used to mock a Struct and it could only mock Traits. This was an incorrect assumption. I just needed to omit the impl block.

I ended up having to read the mock! source to understand the required syntax, which is not awesome because reading procedural macro code is like reading Klingon. (I assume Klingon is hard to read).

Once I knew that the mock! macro could generate a Struct and that the Struct would be named MockHandle, then I just needed to type alias it to Handle so that #[double] would have a definition of Handle on both sides of the conditional compilation.

I also learned the hard way that it's very important that nothing ever does an explicit import of anything under dependencies, because without #[double] pulling the wool over the code's eyes, it will all fall apart.

So don't do this:

#[double]
use crate::dependencies::net;

fn some_function(handle: crate::dependencies::net::Handle) -> () { .. }

#[cfg(test)]
mod tests {
  #[double]
  use super::dependencies::net;
  use super::some_function;

  #[tokio::Test]
  async function some_test() {
    let handle = net::Handle::new();
    some_function(handle);  // <-- Type mismatch
    // Wants: crate::dependencies::net::Handle
    // Found: crate::dependencies::mock_net::Handle
    //...
  }

Of course, that's a silly thing to do, but while iterating you might find you did that by accident.

Anyway, leaving this thread open so that someone can tell me if there's a cleaner syntax to do the above. Currently, I still have to (a) have a mock! block for every struct from the dependency that I interact with and (b) include the function signature of every method I interact with on those structs in the mock! block. If there was a magic macro that said: Generate the mocks for this entire module (and that module is from a dependency and not my own crate), then I could save a lot of typing.

Unfortunately there's no inbuilt way to do this; you need to at least specify the shape that you care about to whatever macros you're using. Someone could theoretically build a tool to do codegen based around rustdoc's JSON output or a syn based parser source scrape, but it'd still be somewhat fragile.

The closest "easy" solution would probably be to make a fork of whichever library you want to mock the API of, add the relevant mock macros to the library source, and use cargo dependency overrides to use your fork instead of upstream when testing using mocks.

Alternatively, implement as much of your code as possible sans IO, such that the surface interacting with the upstream is small enough that it doesn't need dedicated mock tests, and can rely on full fat integration testing for sufficient coverage. This isn't always possible, but it's often a preferable design when it is.

3 Likes

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.