-
-
Notifications
You must be signed in to change notification settings - Fork 2.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Advice request: monkeypatch vs mock.patch #4576
Comments
It does seem to come down to personal preference as far as I've seen so far. There's really three options that work well for pytest (imo) so I'll outline them here. Note these are my opinions and not necessarily representative of others involved with the project or any sort of "official" stance. It's possible we should put something together in the documentation since it is a pretty common subject 👍
In code that I write, I tend to stick to I also wonder if pytest should gut |
personally, i despise mock, needing to use it implies a structural error, so i certainly want to keep monkey-patch for when doing controlled changes of 3rd parties not under my control, but for own code - the moment a mock becomes necessary its a indicator that a re-factoring is needed |
I understand this is a complicated topic, and that this ticket is not really a place to discuss the architectural patterns, and that I'm quite late with this reply, but could @RonnyPfannschmidt summarize what you mean by "needing to use it [mock] implies a structural error"? Or could you link to an article that describes the ideology that you phrase here? |
I'm not @RonnyPfannschmidt but here is my opinion on why Monkeypatching, by definition, breaks the abstraction barrier. Some code reaches into some other code and changes it bowls. The code calls A better alternative is to "formalize" the relationship between the test and the code. The conventional way to do it is give the test explicit control over the particular thing it wants to patch, usually using dependency injection. For an example I'll use the post linked by @asottile. In it, they have this code def restart_server(server):
...
def restart_servers_in_datacenter(servers, datacenter):
for server in servers:
if server.endswith("-%s" % datacenter):
restart_server(server) and they want to write a test for The alternative to patching is do something like this: class ServerManager:
# Takes some dependencies itself (for example).
def __init__(self, http_client: HttpClient, auth_token: str) -> None:
self.http_client = http_client
self.auth_token = auth_token
def restart_server(self, server: Server) -> None:
self.http_client.post("restart server", auth_token=self.auth_token, {server: server.id})
def restart_servers_in_datacenter(server_manager: ServerManager, servers, datacenter):
for server in servers:
if server.endswith("-%s" % datacenter):
server_manager.restart_server(server) Now the test doesn't need to patch. It can do this: fake_server_manager = mock.create_autospec(ServerManager)
restart_servers(fake_server_manager, servers, datacenter)
assert len(fake_server_manager.restart_server.calls) == 1 Now the test cannot make mistakes, it most provide its own implementation of the dependency. It is not possible for the real code to run accidentally. And there is no abstraction being broken, no peace is disturbed, just regular argument passing. You can decide to fake at a deeper level, if you want to increase the coverage: fake_http_client = mock.create_autospec(HttpClient)
server_manager = ServerManager(fake_http_client, 'fake token')
restart_servers(server_manager, servers, datacenter)
assert len(fake_server_manager.restart_server.calls) == 1 Sometimes it's beneficial to go "full Java" and do this: class ServerManager(meta=ABCMeta):
@abstractmethod
def restart_server(self, server: Server) -> None: pass
# As before.
class RealServerManager(ServerManager): ...
# Tests can use this.
class FakeServerManager(ServerManager):
def __init__(self) -> None:
self.restarted_servers = {} # type: Set[Server]
def restart_server(self, server: Server) -> None:
self.restarted_servers.add(server) depending on the case. This intersects with general OO good practices for testable code, see here for example. This style of programming is also enforced in the object-capability security model, which I (personally) hope will gain more prominence in the future. As a disclaimer, I should say that sometimes monkeypatching in tests is necessary, for example when dealing with external code you have no control over. And sometimes it is just easiest and putting more effort is not worth it. And sometimes you intentionally want to test some internal detail. I don't mean to be dogmatic. |
@bluetech Thanks for explaining that (and sorry for late response as I'm travelling). It's a good writeup, I agree with that. However: The distinction you make in your post is monkeypatching vs dependency injection / inversion of control (whatever we want to call it). My question, however, was (what I thought RonnyPfannschmidt was referring to) about mocking vs not mocking (using mock objects, not
Maybe I'm interpreting this wrong, but to me it seems that he says "mock objects are bad" but "monkeypatching (either mock.patch or pytest monkeypatch) is good"? |
i consider both practices bad for different reasons, i consider mock objects bad because they encode expectations that may eventually differ with real systems, and generally try to have in memory or limited interface implementations instead of mocks when possible however even if there is a systematic weakness to both approaches, they still win over playing architecture astronaut so there is a number of situations where the use of those approach beats making them structurally possible for YAGNI or no controll of the upstream anyway |
Since I've started this discussion, allow me to share what I've learned from experience over the past year a bit. (The examples below are real use cases from the codebase, stripped of project specifics and simplified for clarity. Pundits may offer better solutions for each case, but I'll stand by my examples, they are the work of several smart individuals, constrained by requirements of a particular problem domain, and lived through many PRs) Mocking is a valuable technique, especially for unit tests, that is focused on only one aspect of code under test, for example: def test_reject_ancient():
with mock.patch("time.time", return_value=42):
old_request = sign_request(data)
with pytest.raises(TooOld):
verify_request(old_request) Here, something specific is patched just to set up some tiny detail. I don't care about the exact value of time, as long as it's way long ago. The "cost" of the tight coupling in the test is justified by keeping the implementation simple. Which can be extended to something a bit more complex (may or may not be the best choice): @asynctest.mock.patch("mymodule.backend.SomeSideEffect", autospec=True)
async def test_simple_login(_, ephemeral_server, test_user):
async with aiohttp.ClientSession() as session:
async with session.get(ephemeral_server.url) as resp:
assert resp.status == 200 Here, a simple test is done over 2 fixtures and 1 other thing is mocked away, because it's outside of test scope. As test complexity and purpose gets closer to functional (or integration) testing, fixtures rule, and some fixtures are likely to ** monkey-patch**, for example:
Here, fixtures and fixture dependencies are used extensively to control the "life cycle" of the monkey patches. The fixtures are essentially fake objects and perhaps, more generally, test doubles and their implementation relies on mokeypatch to insert (and later remove) the double into the codebase. Note that nowhere here I've seemingly concerned myself with theoretical difference between mocking and monkeypatching. I'm being practical and use whatever works best (🎉 |
Yes, I misread what @RonnyPfannschmidt said. FWIW I think about the opposite -- I try to avoid patching, but I'm perfectly OK with If I apply my suggestion to your examples, then I would avoid Instead of with mock.patch("time.time", return_value=42):
old_request = sign_request(data) I would have For For |
I also want to point out that sometimes (especially for common libraries or usecases), there are third-party patching solutions which avoid having to deal with mocks by hand. I think they can make a lot of sense when dealing with things which are inherently "in the way" (like external HTTP services). In those cases, changing the code to pass in e.g. fake responses might mean those responses aren't the same as they would be in reality, and the "over-generalization" might lead to much more complex code. Some examples I'm aware of: |
I was just about to ask the same question: In Python 3.6+, does pytest.monkeypatch provide any value over unittest.mock.patch? A small concrete example would be pretty awesome 🙂 I have seen the Monkeypatching/mocking modules and environments article (and the linked article) and was wondering if this is only interesting for applictions which have to handle Python versions before Python 3.3 where It would be awesome if you could help me here. If it's desired, I can make a DOC-PR to add the outcome. I didn't completely read this issue as most of the discussions seemed to be about "is mocking a sign for bad code". However, I was confused in the beginning by @asottile stating |
@MartinThoma It boils down to "it's just much more simple to use, with less magic involved" in my eyes. You get a pytest fixture (rather than a decorator), and it's essentially just |
I believe there's no official recommendation because it's really about opinions and trade offs -- for instance I will never use |
Cool, thank you @The-Compiler and @asottile ! (I miss "thank you" as a Github reaction :-) ) As people might stumble over this via Google searches: I can recommend Demystifying the Patch Function (Lisa Roach, Pycon 2018) if you just get started with patching / MagicMock + spec / autospec / spec_set. She explains this really well. |
As of 2022 |
Thanks everyone for the great discussion, but I think we can close this as there's no actionable item. 👍 |
I'm working on a codebase where tests use a mixture of
mock.patch
and pytest'smonkeypatch
, seemingly based on authors' personal preferences.The official docs for the latter, https://docs.pytest.org/en/latest/monkeypatch.html, refer to a blog post that's nearing its 10th anniversary; meanwhile the earlier made it into Python proper.
I wonder if there's official advice, like "use X", or perhaps "if you need feature Y, use Z" to choose between the two.
The text was updated successfully, but these errors were encountered: