{
  "$type": "site.standard.document",
  "canonicalUrl": "https://rednafi.com/python/patch-pydantic-settings-in-pytest/",
  "description": "Mock pydantic_settings in pytest tests by patching the settings class to prevent flaky tests from environment variable dependencies.",
  "path": "/python/patch-pydantic-settings-in-pytest/",
  "publishedAt": "2024-01-27T00:00:00.000Z",
  "site": "at://did:plc:fgtm2c26vfcj74rfmeggbyqj/site.standard.publication/3mnl6f7ob462z",
  "tags": [
    "Python",
    "TIL",
    "Testing",
    "Pytest"
  ],
  "textContent": "I've been a happy user of [pydantic] settings to manage all my app configurations since the\n1.0 era. When pydantic 2.0 was released, the settings portion became a separate package\ncalled [pydantic_settings].\n\nIt does two things that I love: it automatically reads the environment variables from the\n.env file and allows you to declaratively convert the string values to their desired types\nlike integers, booleans, etc.\n\nPlus, it lets you override the variables defined in .env by exporting them in your shell.\n\nSo if you have a variable called FOO in your .env file like this:\n\nThen you can override it via:\n\nAnd pydantic settings will automatically pick up the overridden values without much fuss.\n\nThis is neat but can make writing deterministic unit tests tricky. If the settings instance\nimplicitly pulls config values from both the environment file and shell, testing functions\nusing those values can easily become flaky. Also, it's usually frowned upon if your unit\ntests depend on environment variables in general.\n\nConsider this common instantiation workflow of the settings class. Here, we have the\nfollowing app structure:\n\nIn the src/config.py file, we define our settings class as follows:\n\nThen the corresponding values of the environment variables are defined in the .env file.\nPydantic will automatically convert the upper-cased definitions to lower-case.\n\nNext, we instantiate the Settings class in the src/__init__.py file:\n\nFinally, we use the config values in src/main.py:\n\nFrom the root directory, run the main.py file with this command:\n\nThis reveals that pydantic settings is doing its magic--reading the .env file and\noverriding the default config values:\n\nFantastic! But now, testing the read_env function becomes tricky. Normally, you'd try to\npatch the environment variables in a pytest fixture and then test the values like this:\n\nBut the test will fail because we're initializing the Settings class in the\nsrc/__init__.py file and pydantic processes the environment file and variables before\npytest can intervene.\n\nWe want our unit tests to have no dependencies on the environment variables.\n\nYou might say initializing a class in the __init__.py file like that is an anti-pattern\nand all this can be avoided through dependency injection. You'd be right but you'd also be\nsurprised at how many apps with 7+ figure ARR initialize their config classes like that.\n\nSo patching the environment variables doesn't work, what does?\n\nThe idea is to let pydantic do its magic and then reset the attributes of the Settings\ninstance to their default values in a fixture. We also want the user of the fixture to be\nable to override the values of some or all of the environment variables if necessary.\n\nHere's what has worked well for me:\n\nHere, patch_settings is a parametrizable fixture where you can optionally pass values via\npytest.mark.parametrize to override certain config attributes. If you don't override\nanything, the fixture sets the attributes of the Setting instance to their default values\ndefined in the class.\n\nAbove, first we make a copy of the original settings instance. Then we reset the attributes\nof the Setting instance to their default values. Next, we move on to override any values\npassed via the @parametrize decorator. While doing this, we also check for the correct\ntype of the incoming values and raise an error accordingly.\n\nFinally, we yield the patched instance and reset everything back to their original values\nafter a test ends.\n\nYou can use the fixture like this:\n\nIn the first case, we're not overriding anything. So the tests will use the Settings\ninstance with all the default values. In the second test, we're overriding a few values and\nthe read_env function will use the overridden values.\n\nEither way, the tests don't directly depend on the environment variables and it reduces the\nprobability of spooky actions at a distance.\n\nFin!\n\n\n\n\n[pydantic]:\n    https://docs.pydantic.dev/latest/\n\n[pydantic_settings]:\n    https://docs.pydantic.dev/latest/concepts/pydantic_settings/",
  "title": "Patching pydantic settings in pytest"
}