I found myself in this situation this morning: I have an Emailer service, and want to mock it for testing purposes.
I want to be able to track emails sent.
So I wrote this:
struct DummyEmailer {
pub sent: Vec<(String, String)>,
}
impl Emailer for DummyEmailer {
fn send(&self, subject: &str, body: &str) -> Result<(), String> {
self.sent.push((subject.to_string(), body.to_string()));
Ok(())
}
}
Doesn't compile, of course - we need &mut self to do this.
So, I'd be changing the fn signature to support testability. That's never a good thing.
Other languages have this general issue, of course, but I find that it becomes more 'prominent' in Rust due to the confines of the borrow-checker.
Having to use a mut method isn't exactly horrible , but - anyone have a better suggestion?
I did what I normally do to encapsulate mutability:
struct DummyEmailer {
pub sent: Mutex<Vec<(String, String)>>,
}
impl Emailer for DummyEmailer {
fn send(&self, subject: &str, body: &str) -> Result<(), String> {
self.sent
.lock()
.unwrap()
.push((subject.to_string(), body.to_string()));
Ok(())
}
}
2 Likes
jofas
March 12, 2025, 3:06pm
3
Same as you I would use interior mutability in cases where mutating something behind an immutable interface is required, rather than changing the interface for types that are meant to only mock.
2 Likes
kpreid
March 12, 2025, 3:10pm
4
You could also use a channel in this case.
use std::sync::mpsc;
struct DummyEmailer {
tx: mpsc::Sender<(String, String)>,
}
impl Emailer for DummyEmailer {
fn send(&self, subject: &str, body: &str) -> Result<(), String> {
let _ = self.tx.send((subject.to_string(), body.to_string()));
Ok(())
}
}
The test code holds the Receiver
. This is fairly similar to Arc<Mutex<Vec<_>>>
, but the advantage both have over Mutex<Vec<_>>
is that you do not have to be able to retrieve the DummyEmailer
from whatever was using it in order to examine its state.
7 Likes