{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/patch-with-pytest-fixture/",
  "description": "Combine pytest fixtures with unittest.mock.patch for clean, reusable test mocking patterns that integrate seamlessly with pytest's ecosystem.",
  "path": "/python/patch-with-pytest-fixture/",
  "publishedAt": "2022-02-27T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "Testing",
    "Pytest"
  ],
  "textContent": "In Python, even though I adore writing tests in a functional manner via pytest, I still have\na soft corner for the tools provided in the unittest.mock module. I like the fact it's\nbaked into the standard library and is quite flexible. Moreover, I'm yet to see another\nmock library in any other language or in the Python ecosystem that allows you to mock your\ntargets in such a terse, flexible, and maintainable fashion.\n\nSo, in almost all the tests that I write for both my OSS projects and at my workplace, I use\nunittest.mock.patch exclusively for performing mock-patch. Consider this example:\n\nHere, the prepend_random function prepends a random prefix from the choices sequence to\na target string. To accomplish this randomness, I used the random.choice function from\nthe standard library. Now, the question is, how'd you test this. The function\nprepend_random has one global dependency; it's the random.choice function and you'll\nneed to mock it out. Otherwise, you won't be able to test the enclosing prepend_random\nfunction in a determinstic way. Here's how you might test it with pytest:\n\nIf you run pytest -v -s src.py, the test will pass. The unittest.mock.patch function is\nused here to mock the random.choice function and guarantee that it returns a controlled\nvalue instead of a random one. Shaving off this randomness of the random.choice function\nhelps us test the behaviors of the prepend_random function in a more reproducible fashion.\nThe autospec=True makes sure that the behavior of the mocked object - in this case, the\nfunction signature - is the same as the original object.\n\nAnother thing is, you can also use the patch function as a context manager like this:\n\nWhile this works, the situation quickly spirals out of control whenever you need to test out\nmultiple behaviors of a function and you want to achieve loose coupling between the tests by\ndisentangling the behaviors in separate test functions. In that case, you'll need to mock\nout the dependencies in each test function.\n\nThe situation worsens when each of your target functions has multiple dependencies. To be\nspecific, if your target function has m behaviors and n dependencies that need to be\nmocked out, then the number of the patch decorators that practically do the same thing\nwill be m x n. Just for a single function, it'll create a monstrosity similar to this:\n\nNow imagine the situation for multiple target functions with multiple behaviors where\ntesting each behavior requires multiple dependencies. The DRY gods will be furious!\n\nThe situation can be improved by wrapping the tests in a unittest style class and mocking\nout the common dependencies in the class scope as follows:\n\nThe above solution forces us to write unittest style OOP-driven tests and I'd like to\navoid that in my test suite. Also, this approach will mock all the dependencies in the class\nscope and you can't opt-out of mocked dependencies if some of your tests don't need that. We\ncan do better. Let's rewrite a slightly modified version of the above case with\npytest.fixture. Here's how to do it:\n\nThe target function func has two dependencies that need to be mocked-up - dep_1 and\ndep_2. I mocked out the dependencies using the unittest.mock.patch interjector as\ncontext managers while wrapping them in separate functions decorated with the\n@pytest.fixture decorator.\n\nPay attention to the yield statement in the fixture functions. Pytest also allows you to\nuse return statement in fixtures. However, in this case, making the fixture functions\nreturn generator objects was necessary. This way, the fixture function makes sure that the\nteardown logic in the with patch()... block gets executed. Had you used return here, the\nlogic in the __exit__ block of the patch context manager wouldn't have the chance to be\nexecuted. If you replace the yield statement with return and try to run the above\nsnippet, pytest will throw an error.\n\nYou can also write your custom teardown logic after the yield statement and pytest will\nexecute the logic each time the fixture gets executed. It's similar to how teardown works in\nunittest but in a functional and decoupled manner.\n\nThis approach has the following advantages:\n\n- It appeases the DRY gods.\n\n- You won't have to wrap your tests in a class to avoid patching the same objects multiple\n  times.\n\n- This makes the _mocked dependency_ usage more composable. In a test, you can pick and\n  choose which dependencies you need in their mocked form and which dependencies you don't\n  want to be mocked. If you don't want a particular dependency to be mocked in a test, then\n  don't pass the corresponding fixture as an argument of the test function.\n\n- If some of your mocked dependencies don't vary much during their test lifetime, you can\n  change the scope of the fixture to speed up the overall execution. By default, fixtures\n  run in function scope; that means, the fixture (mocking) will be executed once per test\n  function. This behavior can be changed via using the scope parameter of the\n  @pytest.fixture(scope=...) decorator. Other allowed scopes are module and session.\n  Module scope means, the fixture will be executed once per test module and session\n  scope means, the fixture will run once per pytest session.\n\nAnother practical example\n\nThe following snippet defines the get and post functions that make GET and POST\nrequests to a URL respectively. I used the [HTTPx] library to make the requests. Here, the\nfunctions make external network calls to the https://httpbin.org URL:\n\nRunning the snippet will print the functions' respective HTTP response codes. If you look\nclosely, you'll notice that I'm instantiating the client object in the global scope. This\nis because, both the GET and POST API calls share the same header. Let's see how you can\ntest these two functions:\n\nThe get and post function share a global dependency, the client. Also, these functions\nhave side effects - since they make external network calls. We'll have to mock the functions\nin a way so that our tests are completely isolated and they don't make any network calls. I\nmocked out the functions in the mock_get and mock_post fixtures respectively. The\nfunctions are mocked in a way that whenever the original functions are called in the mock\nscope, they'll return consistent values without making any network call.\n\nSince client was instantiated in the global scope, it had to be mocked using the\npatch.object(...) interjector. Also, notice how the mocked-up get and post functions'\nreturn-values mimic their orginal return-values. In the above case, the fixture runs in the\nmodule scope, which implies, they'll only run once in the entire test module. This makes the\ntest session quicker. However, keep in mind that making fixtures run in the module scope has\nalso its demerits. Since the target functions get mocked and stay mocked through the entire\nmodule, it can subtly create coupling between your test functions if you aren't careful.\n\nFurther reading\n\n- [Test async code with pytest-asyncio]\n- [Unittest mock - mock object library]\n\n\n\n\n[httpx]:\n    https://www.python-httpx.org/\n\n[test async code with pytest-asyncio]:\n    https://github.com/rednafi/reflections/issues/73\n\n[unittest mock - mock object library]:\n    https://docs.python.org/3/library/unittest.mock.html",
  "title": "Patching test dependencies via pytest fixture & unittest mock"
}