Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

mock-usage

Level: warn · Article: Tests

Mocks test your assumptions. Real implementations test your code. Those are not the same test.

What it checks

use mockall, faux, mry, unimock or mockall_double; the #[automock] attribute; the mock! macro. This rule is about tests, so it is not relaxed in test code.

Don’t

#![allow(unused)]
fn main() {
use mockall::automock;

#[automock]
trait Database {
    fn find_by_email(&self, email: &Email) -> Option<User>;
}

#[test]
fn registers_a_new_user() {
    let mut db = MockDatabase::new();
    db.expect_find_by_email().return_const(None); // "no duplicate, go ahead"
    assert!(Users::new(db).register(Email::parse("ada@example.com")).is_ok());
}
}

The unique constraint on email fires in production. The mock never knew what the database contained, because it was not a database.

Do

#![allow(unused)]
fn main() {
struct MemDatabase {
    users: Mutex<HashMap<UserId, User>>,
}

impl Database for MemDatabase {
    fn find_by_email(&self, email: &Email) -> Option<User> {
        let users = self.users.lock().unwrap_or_else(PoisonError::into_inner);
        users.values().find(|user| user.email == *email).cloned()
    }

    fn insert(&self, user: NewUser) -> Result<User, DbError> {
        let mut users = self.users.lock().unwrap_or_else(PoisonError::into_inner);
        if users.values().any(|existing| existing.email == user.email) {
            return Err(DbError::UniqueViolation("email"));
        }
        let user = User::from(user);
        users.insert(user.id.clone(), user.clone());
        Ok(user)
    }
}

#[test]
fn rejects_a_duplicate_email() {
    let db = MemDatabase::default();
    db.insert(NewUser::named("ada", "ada@example.com")).unwrap();
    let duplicate = db.insert(NewUser::named("ada again", "ada@example.com"));
    assert!(matches!(duplicate, Err(DbError::UniqueViolation("email"))));
}
}

Two hours once per dependency. It enforces the same constraints, runs in milliseconds, and earns its place: local dev, seeding, CI without Docker.

Silence it

#![allow(unused)]
fn main() {
// rabot: allow-file(mock-usage) legacy suite, replaced by MemGateway under TEST-88
}