Test code impacting non-test code

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

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

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