Description
What's the problem this feature will solve?
In my organisation, we have a lot of tests split between unittest.mock.patch
, pytest-mock and monkeypatch.
As we try to standardise usage to monkeypatch, we end up with a lot of cases looking like:
def my_test(monkeypatch: pytest.MonkeyPatch) -> None:
mock_foobar: Mock = MagicMock()
monkeypatch.setattr(foo, "bar", mock_foobar)
# ...
There are two main reasons we do this:
- To inject Exceptions into unit tests (i.e.
mock_foobar.side_effect = MyException("...")
). - To assert based on specific calls to the patched attribute.
Describe the solution you'd like
Support mocking in monkeypatch
. I'm happy to contribute this feature, although maybe this is not something you want to actively support.
I propose introducing a new function to monkeypatch
called mockattr
(to match setattr
). This function would patch the attribute to be an instance of MagicMock or AsyncMock (depending on whether the patched attribute is awaitable or not). Potentially new functions for mockitem
and mockenv
should be introduced too for parity for setitem
and setenv
.
def mockattr(
self,
target: str | object,
name: str | notset = notset,
raising: bool = True,
) -> MagicMock | AsyncMock:
# ...
Taking my example above, the code would be simplified slightly. It would also prevent having to know to manually instantiate an instance of MagicMock vs AsyncMock depending on the return type of the function.
def my_test(monkeypatch: pytest.MonkeyPatch) -> None:
mock_foobar: Mock = monkeypatch.mockattr(foo, "bar")
# ...
Alternative Solutions
What I have described above is essentially what we have done internally in my organisation by creating a pytest fixture that exposes mockattr
. However we end up relying on monkeypatch internals (specifically derive_importpath
) in order to inspect the attribute before monkeypatching it to determine whether it is awaitable or not, which we would like to avoid.
An alternative solve, would be to update pytest-mock to use monkeypatch, so that it has similar patching semantics as monkeypatch instead of using unittest patch semantics. However that would most likely result in breaking changes, and I think it's valid for that plugin to continue to exist as is (if people want to use unittest.mock
semantics within pytest
).
Another solve to simplify these use cases, would be to have setattr
return the patched value, which would result in code that looked like this:
def my_test(monkeypatch: pytest.MonkeyPatch) -> None:
mock_foobar: Mock = monkeypatch.setattr(foo, "bar", MagicMock())
# ...
Additional context
I see some similar conversations from the past:
#4576
pytest-dev/pytest-mock#9