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
}