Issue
I'm trying to test my FastAPI
code using pytest. some of the tests I'm doing require the application be in an initial state (some configuration to be reset, object data to be cleared and so on).
both of the methods I tried put the app in the same state during the test session, I also tried to reload the module in which the app is in, but with no success.
here is a minimal reproducible example of the code:
file :main.py
from fastapi import FastAPI
app = FastAPI()
my_state = False
@app.get("/my_state/")
def return_my_state():
return {"state": my_state }
@app.post("/my_state/")
def set_my_state(content:dict):
global my_state
my_state = content['set']
file: test_file1.py
from fastapi.testclient import TestClient
from main import app
import pytest
client = TestClient(app)
@pytest.mark.asyncio
async def test_check_status():
global client
client.post("/my_state/", json={"set": True})
print(client.get("/my_state/").json()['state']) #prints True
file: test_file2.py
from fastapi.testclient import TestClient
from main import app
import pytest
client = TestClient(app)
@pytest.mark.asyncio
async def test_check_status_other_file():
global client
print(client.get("/my_state/").json()['state']) #prints True
this prevents me from properly testing the application. is there another way that I can use other than running the application outside of test cases ??
Solution
If you convert main.py
to use an app factory pattern, and store your state in the app rather than in a global, like this:
from fastapi import FastAPI
def create_app():
app = FastAPI()
app.state.my_state = False
@app.get("/my_state/")
def return_my_state():
return {"state": app.state.my_state}
@app.post("/my_state/")
def set_my_state(content: dict):
app.state.my_state = content["set"]
return app
app = create_app()
Then you can have pytest fixtures that create per-test instances of the app and test client. In conftest.py
we have:
import pytest
from fastapi.testclient import TestClient
from main import create_app
@pytest.fixture
def app():
return create_app()
@pytest.fixture
def client(app):
return TestClient(app)
And then test_file1.py
becomes:
from fastapi.testclient import TestClient
from main import app
import pytest
@pytest.mark.asyncio
async def test_check_status(client):
client.post("/my_state/", json={"set": True})
assert client.get("/my_state/").json()['state']
And test_file2.py
becomes:
from fastapi.testclient import TestClient
from main import app
import pytest
@pytest.mark.asyncio
async def test_check_status_other_file(client):
assert not client.get("/my_state/").json()["state"]
Running pytest -v
produces:
============================= test session starts ==============================
platform linux -- Python 3.11.4, pytest-7.4.0, pluggy-1.2.0 -- /home/lars/tmp/python/.venv/bin/python
cachedir: .pytest_cache
rootdir: /home/lars/tmp/python/testapi
plugins: xdist-3.3.1, mock-3.11.1, anyio-3.7.1, asyncio-0.21.0
asyncio: mode=Mode.STRICT
collecting ... collected 2 items
test_file1.py::test_check_status PASSED [ 50%]
test_file2.py::test_check_status_other_file PASSED [100%]
============================== 2 passed in 0.03s ===============================
This structure ensures that every test starts with a fresh copy of the application with no state retained from previous tests.
All the code referenced in this answer is available in this github repository.
Answered By - larsks
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.