Skip to content

prefer-injected-dependency-over-monkeypatch

Prefer explicit test dependencies; justify scoped attribute patches at the boundary.

Why

Attribute patching hides collaborators and configuration behind ambient module or object state, coupling tests to lookup locations instead of an explicit boundary. Replacing a scoped patch with bare assignment does not fix that coupling and loses automatic restoration. Pytest monkeypatch remains appropriate for reversible process state such as environment variables and the working directory.

Fix

Pass the collaborator, settings, clock, sleeper, random source, client, or factory explicitly; prefer a real in-process dependency, framework override, or purpose-built ABC/Protocol fake. A scoped patch may replace an existing constructor-injected collaborator on a test-owned instance with an interface-conforming fake when rebuilding the fixture would add wiring without improving the boundary. Keep attribute-existence checks enabled and the tested implementation intact. Suppress SARJ445 only at that call, naming the owned instance and injection seam. For inherently global boundaries or lookup interception, explain why injection would invalidate the test. Do not switch to bare assignment merely to avoid this rule.

Examples

Before — flagged Avoid replacing the SDK constructor lookup
tests/test_adapter.py
import pytest
from unittest.mock import patch
def test_constructor_failure():
with patch("transport.Client", side_effect=ConnectionError):
with pytest.raises(AdapterUnavailable):
open_adapter()
After — preferred Inject a factory to exercise constructor failure
tests/test_adapter.py
import pytest
def unavailable_client():
raise ConnectionError
def test_constructor_failure():
with pytest.raises(AdapterUnavailable):
open_adapter(client_factory=unavailable_client)
Before — flagged Test replaces an application collaborator
tests/test_checkout.py
def test_checkout(monkeypatch):
monkeypatch.setattr(checkout, "charge_card", lambda _card: True)
assert checkout.run() == "paid"
After — preferred Test injects a purpose-built collaborator
tests/test_checkout.py
def test_checkout(recording_gateway):
service = CheckoutService(payment_gateway=recording_gateway)
assert service.run() == "paid"