How to override "env_file" during tests?
Question:
I’m reading env variables from .prod.env
file in my config.py:
from pydantic import BaseSettings
class Settings(BaseSettings):
A: int
class Config:
env_file = ".prod.env"
env_file_encoding = "utf-8"
settings = Settings()
in my main.py I’m creating the app
like so:
from fastapi import FastAPI
from app.config import settings
app = FastAPI()
print(settings.A)
I am able to override settings variables like this in my conftest.py
:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import settings
settings.A = 42
@pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
This works fine, whenever I use settings.A
I get 42.
But is it possible to override the whole env_file
from .prod.env
to another env file .test.env
?
Also I probably want to call settings.A = 42
in conftest.py before I import app
, right?
Answers:
You can override the env file you use by creating a Settings
instance with the _env_file
keyword argument.
From documentation:
Passing a file path via the _env_file
keyword argument on instantiation (method 2) will override the value (if any) set on the Config
class. If the above snippets were used in conjunction, prod.env would be loaded while .env would be ignored.
For example, this should work for your test –
import pytest
from fastapi.testclient import TestClient
import app.config as conf
from app.config import Settings
# replace the settings object that you created in the module
conf.settings = Settings(_env_file='.test.env')
from app.main import app
# just to show you that you changed the module-level
# settings
from app.config import settings
@pytest.fixture(scope="module")
def test_client():
with TestClient(app) as client:
yield client
def test_settings():
print(conf.settings)
print(settings)
And you could create a .test.env
, set A=10000000
, and run with
pytest -rP conftest.py
# stuff
----- Captured stdout call -----
A=10000000
A=10000000
This looks a little messy (though this is probably only used for test purposes), so I’d recommend not creating a settings
object that is importable by everything in your code, and instead making it something you create in, say, your __main__
that actually creates and runs the app, but that’s a design choice for you to make.
One workaround I have found is to remove the env_file
from Config completely and replace it’s functionality with load_dotenv()
from dotenv like this:
config.py:
from pydantic import BaseSettings
from dotenv import load_dotenv
load_dotenv(".prod.env")
class Settings(BaseSettings):
A: int
settings = Settings()
conftest.py:
import pytest
from fastapi.testclient import TestClient
from dotenv import load_dotenv
load_dotenv("test.env")
from app.config import settings
from app.main import app
@pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
Please note, that calling load_dotenv("test.env")
happens before importing the settings (from app.config import settings
)
and also note that load_dotenv() will load environment variables globally for the whole python script.
Loading env variables like this will not override already exported variables, same as using the env_file
in pydantic’s BaseSettings
I ran into the same issue today, really annoying. My goal was to set a different postgresql database for the unit tests. The default configuration comes from a .env
file. But when you think of it thoroughly it is not that difficult to understand. It all boils down to the order of the imported modules in conftest.py
. I based the example below on the answer of @wkl:
import pytest
from typing import Generator
from fastapi.testclient import TestClient
import app.core.config as config
from app.core.config import Settings
# Replace individual attribute in the settings object
config.settings = Settings(
POSTGRES_DB="test_db")
# Or replace the env file in the settings object
config.settings = Settings(_env_file='.test.env')
# All other modules that import settings are imported here
# This ensures that those modules will use the updated settings object
# Don't forget to use "noqa", otherwise a formatter might put it back on top
from app.main import app # noqa
from app.db.session import SessionLocal # noqa
@pytest.fixture(scope="session")
def db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
@pytest.fixture(scope="module")
def client() -> Generator:
with TestClient(app) as c:
yield c
I’m reading env variables from .prod.env
file in my config.py:
from pydantic import BaseSettings
class Settings(BaseSettings):
A: int
class Config:
env_file = ".prod.env"
env_file_encoding = "utf-8"
settings = Settings()
in my main.py I’m creating the app
like so:
from fastapi import FastAPI
from app.config import settings
app = FastAPI()
print(settings.A)
I am able to override settings variables like this in my conftest.py
:
import pytest
from fastapi.testclient import TestClient
from app.main import app
from app.config import settings
settings.A = 42
@pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
This works fine, whenever I use settings.A
I get 42.
But is it possible to override the whole env_file
from .prod.env
to another env file .test.env
?
Also I probably want to call settings.A = 42
in conftest.py before I import app
, right?
You can override the env file you use by creating a Settings
instance with the _env_file
keyword argument.
From documentation:
Passing a file path via the
_env_file
keyword argument on instantiation (method 2) will override the value (if any) set on theConfig
class. If the above snippets were used in conjunction, prod.env would be loaded while .env would be ignored.
For example, this should work for your test –
import pytest
from fastapi.testclient import TestClient
import app.config as conf
from app.config import Settings
# replace the settings object that you created in the module
conf.settings = Settings(_env_file='.test.env')
from app.main import app
# just to show you that you changed the module-level
# settings
from app.config import settings
@pytest.fixture(scope="module")
def test_client():
with TestClient(app) as client:
yield client
def test_settings():
print(conf.settings)
print(settings)
And you could create a .test.env
, set A=10000000
, and run with
pytest -rP conftest.py
# stuff
----- Captured stdout call -----
A=10000000
A=10000000
This looks a little messy (though this is probably only used for test purposes), so I’d recommend not creating a settings
object that is importable by everything in your code, and instead making it something you create in, say, your __main__
that actually creates and runs the app, but that’s a design choice for you to make.
One workaround I have found is to remove the env_file
from Config completely and replace it’s functionality with load_dotenv()
from dotenv like this:
config.py:
from pydantic import BaseSettings
from dotenv import load_dotenv
load_dotenv(".prod.env")
class Settings(BaseSettings):
A: int
settings = Settings()
conftest.py:
import pytest
from fastapi.testclient import TestClient
from dotenv import load_dotenv
load_dotenv("test.env")
from app.config import settings
from app.main import app
@pytest.fixture(scope="module")
def test_clinet():
with TestClient(app) as client:
yield client
Please note, that calling load_dotenv("test.env")
happens before importing the settings (from app.config import settings
)
and also note that load_dotenv() will load environment variables globally for the whole python script.
Loading env variables like this will not override already exported variables, same as using the env_file
in pydantic’s BaseSettings
I ran into the same issue today, really annoying. My goal was to set a different postgresql database for the unit tests. The default configuration comes from a .env
file. But when you think of it thoroughly it is not that difficult to understand. It all boils down to the order of the imported modules in conftest.py
. I based the example below on the answer of @wkl:
import pytest
from typing import Generator
from fastapi.testclient import TestClient
import app.core.config as config
from app.core.config import Settings
# Replace individual attribute in the settings object
config.settings = Settings(
POSTGRES_DB="test_db")
# Or replace the env file in the settings object
config.settings = Settings(_env_file='.test.env')
# All other modules that import settings are imported here
# This ensures that those modules will use the updated settings object
# Don't forget to use "noqa", otherwise a formatter might put it back on top
from app.main import app # noqa
from app.db.session import SessionLocal # noqa
@pytest.fixture(scope="session")
def db() -> Generator:
try:
db = SessionLocal()
yield db
finally:
db.close()
@pytest.fixture(scope="module")
def client() -> Generator:
with TestClient(app) as c:
yield c