This is a simplified version of my problem.
I have a python application with the structure as below:
my_project
my_app
__init__.py
settings.py
tests
__init__.py
conftest.py
my_test.py
venv
# settings.py
from dotenv import load_dotenv
load_dotenv()
MY_VALUE = os.environ["MY_KEY"]
I dont want to add a .env file in here for some reasons. I also dont want to use get method on os.environ
I want to run this test
# my_test.py
from my_app import settings # I need this for some reasons
def test_something():
assert True
def test_env():
assert os.environ["MY_KEY"] == 'MY_VALUE'
However, I get a KeyError because once I import settings.py, the line MY_VALUE = os.environ["MY_KEY"] is run and because MY_KEY is not in env, I get KeyError.
What I thought is that I could have conftest as below
import os
from unittest import mock
import pytest
@pytest.fixture(autouse=True)
def set_env(config):
with mock.patch.dict(os.environ, {'MY_KEY': 'MY_VALUE'}):
yield
But it is still the same problem.
How can I enforce this conftest before everything else.
also note that if I comment out from my_app import settings both my tests will pass
NOTE: the reason I do not want to use os.environ.get("MY_KEY") is that I want to enforce env variable to exist before the application or the test is initialized. The reason for this is that in reality, when my application is running in a docker container, the env variables are there before everything else and I want to increase dev-prod parity. https://12factor.net/dev-prod-parity
This occurs because pytest first collects all tests and then runs them.
During the collection phase, pytest imports conftest.py and my_test.py.
During the test running phase, pytest applies fixtures and runs the test functions themselves.
In your case, during the collection phase, my_test.py imports my_app/settings.py, resulting in the side effect of reading the environment variable os.environ["MY_KEY"]. Since fixtures have not been applied yet, a KeyError is raised.
There are two ways to resolve this issue:
Instead of initializing the settings inside the module, the initialization could be moved to the get_settings function. Additionally, functools.cache can be used to ensure that the configuration is loaded only once.
# my_app/settings.py
import functools
from dotenv import load_dotenv
@functools.cache
def get_settings():
load_dotenv()
MY_VALUE = os.environ["MY_KEY"]
return {"MY_KEY": MY_VALUE}
# tests/conftest.py
import os
from unittest import mock
import pytest
@pytest.fixture(autouse=True)
def set_env():
with mock.patch.dict(os.environ, {'MY_KEY': 'MY_VALUE'}):
yield
# tests/my_test.py
from my_app import get_setting
def test_env():
settings = get_setting()
assert settings["MY_KEY"] == 'MY_VALUE'
settings.pyIf the first point is not applicable, settings.py could be imported inside a test. This way, it will be imported in the test run phase, after the fixture has been applied.
# my_app/settings.py
from dotenv import load_dotenv
load_dotenv()
MY_VALUE = os.environ["MY_KEY"]
# tests/conftest.py
import os
from unittest import mock
import pytest
@pytest.fixture(autouse=True)
def set_env():
with mock.patch.dict(os.environ, {'MY_KEY': 'MY_VALUE'}):
yield
# tests/my_test.py
def test_env():
from my_app import settings
assert settings.MY_KEY == 'MY_VALUE'
Note that Python will cache the settings after the first import. If you want to run multiple tests with different settings, you could:
# tests/conftest.py
import os
import importlib
from unittest import mock
import pytest
@pytest.fixture
def set_env_1():
with mock.patch.dict(os.environ, {'MY_KEY': 'MY_VALUE_1'}):
yield
@pytest.fixture
def set_env_2():
with mock.patch.dict(os.environ, {'MY_KEY': 'MY_VALUE_2'}):
yield
@pytest.fixture
def settings():
from py_helpers import settings
return importlib.reload(settings)
# tests/my_test.py
def test_env_my_key_1(set_env_1, settings):
assert settings.MY_KEY == 'MY_VALUE_1'
def test_env_my_key_2(set_env_2, settings):
assert settings.MY_KEY == 'MY_VALUE_2'
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With