pytest-dev / pytest-dev/pytest
Add explicit support for `unittest.mock.Mock` in monkeypatch fixture
Nobody has claimed this yet.
- Dominant language
- Python
- Stars
- 14.5k
- Forks
- 3.4k
- Avg merge
- 2d 9h
- Merged PRs (30d)
- 35
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:
https://github.com/pytest-dev/pytest/issues/4576
https://github.com/pytest-dev/pytest-mock/issues/9
Contributor guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
Research direction
Start by reading pytest's monkeypatch fixture, especially setattr and the derive_importpath path mentioned in the issue. Review issues #4576 and pytest-mock #9, then determine which API is accepted for mocking synchronous and asynchronous attributes. Done means an agreed scope, implementation, and tests covering the selected behavior.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- python
- Domain
- testing
- Issue type
- Feature
- Difficulty
- 5/5
- Estimated time
- Over a week
- Activity status
- Stale
- Clarity
- Mostly clear
- Newbie friendliness
- 35/100