Beginner question: How to mock std::net::UdpSocket?

I am still getting started with Rust, and I am trying to write a small and trivial library that will perform Udp broadcasts, according to the SSDP protocol, and parse everything that will come back.

So far, everything works exactly like I intended. But I am stuck at writing unit tests for the parts that work on a std::net::UdpSocket. The methods I want to test accept a UdpSocket as a parameter, and then broadcast things on that very socket. In my tests, I want to pass a mock object into it, which will record the data and addresses which were broadcasted to.

The problem is that UdpSocket implements no trait, there's just methods. I thought of adding a separate trait, copy the needed methods in there so that UdpSocket is a valid implementation. However, the signatures like send_to(&self, buf: &[u8], addr: SocketAddr) are now getting in my way, because it is all &self and not mutable, so I cannot store a copy of the buffer that was sent to alter validate that in my unit tests.

I would like the user of my library to stick with the original UdpSocket from the standard library, instead of adding another layer of abstraction.

I am stuck and don't know how to proceed. I found some (rather old) Rust libraries that have already dealt with the SSDP protocol, but neither of those had any unit tests at all.

Personally I don't bother with mocking Rust types. I'm not familiar with SSDP but when testing code that makes network calls I'll set up a socket/server in the test code and give its address to the code being tested. That way the code being tested works just as it does in production and only the endpoint(s) it communicates with are fake.

3 Likes

Why not use real a socket over localhost? It performs very well.

You can use the port zero to ask the OS to give you a random unused port.

3 Likes

Another possibility is to design your library using the sans io design pattern. Here, you write a library that just operates on byte slices and doesn't do any IO. Then, you write a very simple wrapper that moves data between the socket and the library. This way, you don't need to touch sockets or io types in your tests.

See the link for more ideas on this approach.

8 Likes

Thank you very much for your replies @Alice and @Heliozoa.

Yes, for 1:1 connection over TCP, that's something I would have done without a doubt.

However, SSDP is a discovery protocol that will broadcast. And I would like to test that my library actually does the correct broadcasts for IPv6 and IPv4. For me at home, that is not really a problem, but I think I should not randomly trigger network-wide broadcasts on other peoples machines and build systems.

I will absolutely read up on that, thank you very much!

1 Like

You could run the tests in a container with an isolated network. That way it won't escape to the host network.
The noisy tests can check if they're sufficiently isolated and otherwise bail out early.

If those methods instead took a generic

where
    T: Into<U>,
    U: SocketInterface

SocketInterface is a trait that has all the methods you call on the socket

You make one type MyUdpSocket wrapping UdpSocket that it forwards the trait methods onto.

You impl From<UdpSocket> for MyUdpSocket and users will pass in UdpSocket none the wiser.

Finally you make a second type MockUdpSocket and implement the same trait, only this time you can use inner mutation (like RefCell) to update your logs and whatnot for testing what packets it would send.

It's a pain! And I'm not 100% sure it works, not tested it in a playground yet, the kinda weird trait bounds might be problematic... But it could be worth a shot. I'll see if I can tackle a demo tomorrow, I'm curious how it pans out :slight_smile:

P.S You should consider sealing that trait since it will be in the public API, but you don't really want to encourage users to try implementing it!

Gosh I'm being silly! It wouldn't go in the public API at all.

Any public methods taking a UDP socket now still do, but become a thin wrapper around your generic method that does the real work using a generic signature, and that one gets tested with the mock. (Hope that makes sense!)

Sort of the reverse of the usual trick to accept a generic param and forward to a concrete method to reduce binary size from monomorphisation :smiley:

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.