use crate::code_lint::ast::ParsedFile;
use crate::code_lint::contract::{CodeRule, RuleTarget};
use crate::diagnostic::{Diagnostic, Language, RuleName, ViolationTemplate, violation_template};
use crate::rule_declaration::{
Classification, Consensus, Declaration, Example, FilterListDefaults, ImpactedQuality, ListKind,
ListOption, Precision, Reference, RuleDoc, RuleOptions, Topic,
};
use std::collections::HashSet;
use std::path::Path;
const BANNED: ListOption = ListOption {
kind: ListKind::Deny,
doc: "Mocking and monkeypatching calls flagged in tests.",
default: FilterListDefaults {
base: &[
"Mock",
"MagicMock",
"AsyncMock",
"NonCallableMock",
"PropertyMock",
"create_autospec",
"patch",
"patch.object",
"patch.dict",
"patch.multiple",
"mock.Mock",
"mock.MagicMock",
"mock.AsyncMock",
"mock.NonCallableMock",
"mock.PropertyMock",
"mock.create_autospec",
"mock.patch",
"mock.patch.object",
"mock.patch.dict",
"mock.patch.multiple",
"unittest.mock.Mock",
"unittest.mock.MagicMock",
"unittest.mock.AsyncMock",
"unittest.mock.NonCallableMock",
"unittest.mock.PropertyMock",
"unittest.mock.create_autospec",
"unittest.mock.patch",
"unittest.mock.patch.object",
"unittest.mock.patch.dict",
"unittest.mock.patch.multiple",
"mocker.Mock",
"mocker.MagicMock",
"mocker.AsyncMock",
"mocker.NonCallableMock",
"mocker.PropertyMock",
"mocker.create_autospec",
"mocker.patch",
"mocker.patch.object",
"mocker.patch.dict",
"mocker.patch.multiple",
"mocker.spy",
"mocker.stub",
"mocker.async_stub",
"MonkeyPatch",
"MonkeyPatch.context",
"pytest.MonkeyPatch",
"pytest.MonkeyPatch.context",
"monkeypatch.setattr",
"monkeypatch.delattr",
"monkeypatch.setitem",
"monkeypatch.delitem",
],
extend: &[],
remove: &[],
},
};
const TEMPLATE: ViolationTemplate = violation_template! {
summary: "Test mocks or patches with `{callee}()`.",
rationale: "A dynamic mock or patch couples the test to the internal call wiring and keeps passing when the real dependency's signature or contract changes.",
suggestion: "Inject an in-memory fake (such as `FakeRepository` or `FakeHttpClient`) that implements the dependency's `Protocol`.",
};
pub const RULE: CodeRule<ListOption> = CodeRule {
declaration: Declaration {
name: RuleName("mock-in-tests"),
template: &TEMPLATE,
languages: &[Language::Python],
options: RuleOptions::code_rule(BANNED),
classification: Classification {
topics: &[Topic::TEST_DOUBLES],
precision: Precision::Exact,
consensus: Consensus::Opinionated,
impacted_quality: ImpactedQuality::Reliability,
},
doc: RuleDoc {
summary: "Flags dynamic mocks and monkeypatching in Python tests.",
what_it_does: indoc::indoc! {r"
Flags calls that create mocks or patch code at run time, such as `MagicMock()`,
`patch(...)`, `mocker.patch(...)` and `monkeypatch.setattr(...)`."},
why_is_this_bad: indoc::indoc! {r"
A mock replaces a real collaborator with an object that accepts any call and returns
whatever the test told it to. Patching swaps code by its import path. Both tie the
test to how the code is wired internally rather than to what it does, so refactors
break tests that should pass, and tests keep passing when the real dependency
changes its signature or behaviour.
Pass dependencies in explicitly and use a fake in tests: a small working in-memory
implementation of the same interface (for example a `FakeRepository` backed by a
dict), then assert on its state."},
known_problems: None,
references: &[
Reference {
title: "Software Engineering at Google, ch. 13: Test Doubles",
url: "https://abseil.io/resources/swe-book/html/ch13.html",
},
Reference {
title: "Mocks Aren't Stubs (Martin Fowler)",
url: "https://martinfowler.com/articles/mocksArentStubs.html",
},
Reference::NAME_RESOLUTION,
],
examples: &[Example {
language: Language::Python,
flagged: indoc::indoc! {r#"
def test_register_user():
repository = MagicMock()
register_user(repository, "alice@example.com")
assert repository.save.called
"#},
flagged_span: "MagicMock()",
fixed: indoc::indoc! {r#"
def test_register_user():
repository = FakeUserRepository()
register_user(repository, "alice@example.com")
assert "alice@example.com" in repository.users
"#},
}],
},
},
target: RuleTarget::TestsOnly,
check: check_file,
};
fn check_file(
rule: &CodeRule<ListOption>,
path: &Path,
file: &ParsedFile,
banned: &HashSet<String>,
) -> Vec<Diagnostic> {
rule.check_banned_calls(path, file, banned)
}
#[cfg(test)]
crate::test_utils::rule_test!(
RULE,
{
Python => {
pass: [
state_based_fake_repository => r#"
class FakeUserRepository(UserRepository):
def __init__(self):
self.users = {}
def save(self, user):
self.users[user.id] = user
def test_register_user():
repo = FakeUserRepository()
register_user(repo, "alice@example.com")
assert "alice@example.com" in repo.users
"#,
http_client_patch_method => r#"
def test_http_patch_request(http_client):
response = http_client.patch("/users/1", json={"active": True})
assert response.status_code == 200
"#,
unrelated_imported_patch_exempt => r#"
from jsonpatch import patch
def test_json_patch():
result = patch({"a": 1}, [])
assert result == {"a": 1}
"#,
locally_defined_patch_helper_exempt => r#"
def patch(document, operations):
return document
def test_local_patch():
result = patch({"a": 1}, [])
assert result == {"a": 1}
"#,
],
fail: [
magic_mock_instantiation => r#"
from unittest.mock import MagicMock
def test_service():
client = MagicMock()
"# => "MagicMock()",
aliased_unittest_mock_patch => r#"
from unittest.mock import patch as mock_patch
def test_fetch():
with mock_patch("app.service.fetch_data"):
pass
"# => r#"mock_patch("app.service.fetch_data")"#,
unittest_mock_patch_decorator => r#"
from unittest.mock import patch
@patch("app.service.load_config")
def test_fetch(mock_config):
pass
"# => r#"patch("app.service.load_config")"#,
unittest_mock_patch_context_manager => r#"
from unittest.mock import patch
def test_fetch():
with patch("app.service.fetch_data") as mock_fetch:
pass
"# => r#"patch("app.service.fetch_data")"#,
pytest_mocker_patch_object => r#"
def test_overrides(mocker):
mocker.patch.object(Notifier, "send")
"# => r#"mocker.patch.object(Notifier, "send")"#,
pytest_monkeypatch_setattr => r#"
def test_overrides(monkeypatch):
monkeypatch.setattr(settings, "DEBUG", True)
"# => r#"monkeypatch.setattr(settings, "DEBUG", True)"#,
qualified_unittest_mock => r#"
import unittest.mock
def test_service():
client = unittest.mock.Mock()
"# => r#"unittest.mock.Mock()"#,
qualified_mock => r#"
import mock
def test_service():
client = mock.Mock()
"# => r#"mock.Mock()"#,
pytest_monkeypatch_context => r#"
import pytest
def test_overrides():
with pytest.MonkeyPatch.context() as mp:
pass
"# => r#"pytest.MonkeyPatch.context()"#,
],
},
}
);