Skip to content
Sarj Code Standards

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. Establish the binding the exercised consumer actually reads, including already-bound import aliases, and show that the oracle rejects a wrong consumer. 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"
Before — flagged A normalized pytest factory replaces a dependency
tests/test_service.py
import pytest
def test_service():
patcher = pytest.MonkeyPatch()
patcher.setattr(service, "clock", fake)
After — preferred Process environment operations remain excluded
tests/test_service.py
import pytest
def test_service():
patcher = pytest.MonkeyPatch()
patcher.setenv("MODE", "test")