
Clean patching
When it comes to patching and mocking, Python's built-in libraries are amazing. But if you write a lot of tests you will start to hate patching expressions like this decorator:
@patch("myapp.mycode.imported_name")
or this with
statement:
with patch("myapp.mycode.imported_name") as mock_imported_name:
The problems
First of all, they require a lot of typing, and second, they are very fragile.
Typing is prone to typos and copy-paste-errors. And if you refactor your code and move mycode
to another module, you will have to change all the patching expressions in your tests. Although powerful IDEs like PyCharm can help you with that to some extent, manually fixing the remaining patch statements is still a pain.
Good news: you can avoid the whole stuff with clean coding and some Python magic. You can take the magic literally as we will use a so-called magic attribute. (an attribute with double underscores at the beginning and the end of their names, like __doc__
or __name__
.)
So, if you hate unnecessary typing and wasting time fixing your once well-written code at refactor time, read on.
The solution
You can tackle it from three directions. The first one is trivial. If you test a lot, then you are already using it: mock the method instead of patching it like this:
self.test_object.method = Mock()
Unlike patching, it won't reset the method to the original object later, but that shouldn't be a problem in most cases since you should reset your test object between tests with method-level setups anyway.
Then there are the properties. We can't use value assignment on them.

But patch.object
comes to the rescue. You can call it with the class the property belongs to. Since we're talking about unit tests, the class being tested is already imported to be used in the test setup. This will be the solution:
@patch.object(MyClass, "property_to_mock", new_callable=PropertyMock, return_value="mock_value")
def test_some_method_using_the_property(self, mocked_property: Mock) -> None:
pass
Great! We're fine with methods and properties then. But how to deal with patching imported names? This approach works best for explicit imports, like from json import loads
.
class TestMyClass:
namespace = MyClass.__module__
@patch(f"{namespace}.loads") # imported as `from json import loads` in the module
def test_some_method_where_json_loads_is_used(self, mocked_loads: Mock) -> None:
pass
Beautiful, isn't it? The module name is used dynamically, and you can reuse it anywhere. No more novel-length patching expressions that break during refactoring!
Longer self-explanatory code examples you can try out:
src/myapp/mycode.py
from json import loads
class MyCode:
MY_JSON = """{"content": "Hello, World!"}"""
@property
def my_property(self) -> str:
return loads(self.MY_JSON)["content"]
def my_method(self) -> str:
return loads(self.MY_JSON)["content"]
def my_other_method(self) -> str:
return ":)" if self.my_method() == self.my_property else ":("
tests/test_myapp/test_mycode.py
from unittest.mock import Mock, PropertyMock, patch
from myapp.mycode import MyCode
class TestMyCode:
# define this so you don't have hardcode anything when you patch:
namespace = MyCode.__module__
def setup_method(self) -> None:
self.test_object = MyCode()
# Instead of this:
# @patch("myapp.mycode.loads", return_value={"content": "test text"})
# use this:
@patch(f"{namespace}.loads", return_value={"content": "test text"})
def test_my_property(self, mocked_loads: Mock) -> None:
# Act
result = self.test_object.my_property
# Assert
assert result == "test text"
mocked_loads.assert_called_once_with(self.test_object.MY_JSON)
# Instead of this:
# @patch(f"myapp.mycode.MyCode.my_property", new_callable=PropertyMock, return_value="test text")
# use this:
@patch.object(MyCode, "my_property", new_callable=PropertyMock, return_value="test text")
def test_my_other_method(self, mocked_property: Mock) -> None:
# Arrange
# If you reinstantiate the test instance between tests (you should)
# than for method mocking, use Mock directly:
self.test_object.my_method = Mock(return_value="test text")
# Act
result = self.test_object.my_other_method()
# Assert
assert result == ":)"
mocked_property.assert_called_once()
self.test_object.my_method.assert_called_once()
pyproject.toml
[tool.pytest.ini_options]
addopts = "--cov"
pythonpath = ["src"]
testpaths = ["tests"]
[tool.coverage.run]
source = ["src"]
[tool.ruff]
exclude = [".venv"]
lint.ignore = ["E501"]
lint.select = ["E", "F", "W", "C901", "I", "N", "UP", "YTT", "ANN", "SLF", "RET", "TC", "PTH"]
preview = true
target-version = "py313"
requirements.txt
pytest-cov==7.0.0
ruff==0.8.3