How to mock function from external crates?

Let's say I have an Ip struct like this:

/// Individual IP/name tuple
#[derive(Clone, Debug, Eq, Ord, PartialOrd, PartialEq)]
pub struct Ip {
    /// IP, can be IPv4 or IPv6
    pub ip: IpAddr,
    /// hostname.
    pub name: String,
}

I also have a solve()method to convert IP into name using the lookup_addr()function from the dns-lookup crate.

impl Ip {
    // ...
    pub fn solve(&self) -> Self {
        let ip = self.ip;
        let name = match lookup_addr(&ip) {
            Ok(nm) => {
                // No PTR, force one
                if ip.to_string() == nm {
                    "some.host.invalid".into()
                } else {
                    nm
                }
            }
            Err(e) => e.to_string(),
        };
        Ip { ip, name }
    }
}

I'm trying to write a test for Ip::solve() and want to avoid network latencies and timeouts by mocking lookup_addr(). I've found the double and mockall crates which have similar methods for mocking, generally with defining a trait with the function to mock and using a combination of macros to implement the mocking. I can't get it to work at all.

So, how would you do it? The go approach I used before (see here was to have an interface (equiv. to a trait more or less) but Rust is way more complicated. I'm a bit lost.

Complete code is in several branches at dmarc-rs.

Thanks.

You could create a façade to work as the interface between your code and the external dependency, and then mock that instead.

I would try to use the feature flags and conditional compilation.

fn solve_dispatch(&self) -> Self {
  [cfg(feature = "mocked")]
    todo!("simple_algorithm")
  [cfg(not(feature = "mocked"))]
    todo!("real_algorithm")
}

Or something like that.

I would just do DI. Make a trait DnsResolver { fn lookup_addr() -> Result<...> } then have a struct RealDnsResolver that impl DnsResolver for RealDnsResolver, and also a impl DnsResolver for FakeDnsResolver.

Pretty much like what they do in Java and any other OOP language. Interface abstracting side-effects, then injecting it where needed, instead of hardcoding behavior.

I remember Rust traits and dependency injection - Julio Merino (jmmv.dev) was a decent article with some good ideas around the problem. Frankly, I think Rust is lacking here and could use a good and popular library to help with the boilerplate w.r.t defining traits and then using Box/Arc< dyn Trait> objects that implement them, but most people that look at the problem jump straight to building some pointless Springboot-like DI containers and alikes which no one in Rust community will want to use (because they are a bad idea).

2 Likes

That's not very feasible as it prevent switching at run-time (like if you have a --no-resolve flag and neither does it help for testing.

1 Like

Thanks for the reference of the article, I'll read it. In the end, that's how I'm going to go, it is the same approache I have in Go but implicit interfaces and func pointers makes it slightly easier there. Things like Box<dyn Resolver> are not Copy so it makes other stuff harder to have the pointer around.

It doesn't prevent anything.

You can have a

struct SwitchableDnsResolver {
  switch: Arc<AtomicBool>,
  first: Box<dyn DnsResolver>,
  second: Box<dyn DnsResolver>,
}

impl DnsResolver for SwitchableDnsResolver {
  fn resolve(&self, s: &str) -> Result<IP> {
    if self.switch.load() {
       self.first.resolve(s)
   } else {
       self.second.resolve(s)
  }
 }
}

and have a shared switch bool flag switch the behavior of SwitchableDnsResolver from anywhere in your program at runtime. You can literally implement anything.

You need it shared? Arc<dyn Resolver + Send + Sync> it is. .clone() it like there's no tomorrow, send between threads, you name it.

3 Likes

Yes, I need it to be Send because in a fan-out/fan-system with worker threads I want to call it in all threads. Didn't see Arc<> was Send yet, thanks.

After reading the article, I came up with a similar scheme and it seems to be working fine. Thanks.

See: resolver.rs for the trait implementation and its usage in resolve.rs

2 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.