-
First Check
Commit to Help
Example Code"""
test_fastapi.py
Requires:
- python3
- docker
Setup:
virtualenv -p /usr/bin/python3 .venv
source .venv/bin/activate
pip install fastapi==0.69 sqlalchemy mysqlclient pytest requests
Usage:
pytest test_fastapi.py -vvs
"""
import subprocess
import time
import pytest
from fastapi import Body, FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel
from sqlalchemy import (
Column,
Integer,
MetaData,
String,
Table,
create_engine,
insert,
select,
)
# Config for the docker container
CONTAINER = "mariadb-fastapi"
MYSQL_PORT = "33072"
MYSQL_DATABASE = "test"
MYSQL_ROOT_PASSWORD = "rootmysql"
DB_URL = f"mysql://root:{MYSQL_ROOT_PASSWORD}@127.0.0.1:{MYSQL_PORT}/{MYSQL_DATABASE}"
# SQLAlchemy
ENGINE = create_engine(DB_URL)
METADATA = MetaData()
T = Table(
"job",
METADATA,
Column("id", Integer(), primary_key=True),
Column("name", String(length=50)),
)
# FastAPI model and app
class Job(BaseModel):
name: str
app = FastAPI()
@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
conn = ENGINE.connect()
stmt = select([T]).with_for_update().where(T.c.id == job_id)
conn.execute(stmt).fetchone() # The result does not matter
# This exception that is raised in the first test causes the second test to
# hang during its setup.
# If we do "return None" instead, the second test will be run.
# return None
raise HTTPException(404, f'Job with ID "{job_id}" not found')
@app.post("/jobs")
def create_job(job: Job = Body(...)):
with ENGINE.begin() as conn:
ins = insert(T).values(**job.dict())
conn.execute(ins)
#
# Tests
#
@pytest.fixture(autouse=True, scope="module")
def mariadb_docker():
with subprocess.Popen(
[
"docker",
"run",
"--rm",
"--env",
f"MYSQL_DATABASE={MYSQL_DATABASE}",
"--env",
f"MYSQL_ROOT_PASSWORD={MYSQL_ROOT_PASSWORD}",
f"--name={CONTAINER}",
f"--publish={MYSQL_PORT}:3306",
"mariadb",
]
):
print("connecting ", end="")
while True:
try:
ENGINE.connect()
print("done")
break
except:
print(".", end="")
time.sleep(1)
yield
subprocess.run(["docker", "stop", CONTAINER], check=False)
@pytest.fixture
def session():
print("SESSION drop all")
METADATA.drop_all(ENGINE)
print("SESSION create all")
METADATA.create_all(ENGINE)
print("SESSION make client")
client = TestClient(app)
print("SESSION setup done")
return client
def test_function_raising_error(session):
print("TEST starting")
response = session.get(f"/jobs/2")
assert response.status_code == 404, response.text
print("TEST end")
def test_hangs_in_setup(session):
print("TEST starting")
response = session.post(f"/jobs", data={"name": "test"})
assert response.status_code == 422
print("TEST end")
import subprocess
import time
import pytest
from fastapi import Body, FastAPI, HTTPException, Path
from fastapi.testclient import TestClient
from pydantic import BaseModel
from sqlalchemy import (
Column,
Integer,
MetaData,
String,
Table,
create_engine,
insert,
select,
)
# Config for the docker container
CONTAINER = "mariadb-fastapi"
MYSQL_PORT = "33072"
MYSQL_DATABASE = "test"
MYSQL_ROOT_PASSWORD = "rootmysql"
DB_URL = f"mysql://root:{MYSQL_ROOT_PASSWORD}@127.0.0.1:{MYSQL_PORT}/{MYSQL_DATABASE}"
# SQLAlchemy
ENGINE = create_engine(DB_URL)
METADATA = MetaData()
T = Table(
"job",
METADATA,
Column("id", Integer(), primary_key=True),
Column("name", String(length=50)),
)
# FastAPI model and app
class Job(BaseModel):
name: str
app = FastAPI()
@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
conn = ENGINE.connect()
stmt = select([T]).with_for_update().where(T.c.id == job_id)
row = conn.execute(stmt).fetchone()
if row is None:
# This exception that is raised in the first test causes the second test to
# hang during its setup.
# If we do "return None" instead, the second test will be run.
# return None
raise HTTPException(404, f'Job with ID "{job_id}" not found')
return row
@app.post("/jobs")
def create_job(job: Job = Body(...)):
with ENGINE.begin() as conn:
ins = insert(T).values(**job.dict())
conn.execute(ins)
#
# Tests
#
@pytest.fixture(autouse=True, scope="module")
def mariadb_docker():
with subprocess.Popen(
[
"docker",
"run",
"--rm",
"--env",
f"MYSQL_DATABASE={MYSQL_DATABASE}",
"--env",
f"MYSQL_ROOT_PASSWORD={MYSQL_ROOT_PASSWORD}",
f"--name={CONTAINER}",
f"--publish={MYSQL_PORT}:3306",
"mariadb",
]
):
print("connecting ", end="")
while True:
try:
ENGINE.connect()
print("done")
break
except:
print(".", end="")
time.sleep(1)
yield
subprocess.run(["docker", "stop", CONTAINER], check=False)
@pytest.fixture
def session():
print("SESSION drop all")
METADATA.drop_all(ENGINE)
print("SESSION create all")
METADATA.create_all(ENGINE)
print("SESSION make client")
client = TestClient(app)
print("SESSION setup done")
return client
def test_function_raising_error(session):
print("TEST starting")
response = session.get(f"/jobs/2")
assert response.status_code == 404, response.text
print("TEST end")
def test_hangs_in_setup(session):
print("TEST starting")
response = session.post(f"/jobs", data={"name": "test"})
assert response.status_code == 422
print("TEST end")DescriptionStarting with FastAPI 0.69, pytest tests started to hang under obscure conditions. The same code worked as expected with FastAPI 0.68.2. I had issues with hanging tests in multiple projects. One was related to working with temp file handlers and the other one is shown in the example and related to working with SQLAlchemy/MariaDB. In the example, the first test calls an endpoint that does an SQLAlchemy query and raises an Any test after that will hang during setup (when This is what I get when I run the example: $ pytest test_fastapi.py -vvs
=========================================================================================== test session starts ============================================================================================
platform linux -- Python 3.9.7, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 -- /tmp/.venv/bin/python
cachedir: .pytest_cache
rootdir: /tmp
plugins: anyio-3.4.0
collected 2 items
test_fastapi.py::test_function_raising_error connecting done
SESSION drop all
SESSION create all
docker: Error response from daemon: Conflict. The container name "/mariadb-fastapi" is already in use by container "70717ec0e7ee7f8b721c2866a79eca73cfcd7bb3758978a9b7fe8e9b8f008786". You have to remove (or rename) that container to be able to reuse that name.
See 'docker run --help'.
SESSION make client
SESSION setup done
TEST starting
TEST end
PASSED
test_fastapi.py::test_hangs_in_setup SESSION drop allOperating SystemLinux Operating System DetailsFedora 34 FastAPI Version0.69.0 Python Version3.9.7 Additional ContextDocker 20.10.11 |
Beta Was this translation helpful? Give feedback.
Replies: 5 comments
-
|
The issue is not appearing with sqlite, but with mysql+mysqlclient, mysql+pymysql and postgres. |
Beta Was this translation helpful? Give feedback.
-
|
I'm having a similar issue in version |
Beta Was this translation helpful? Give feedback.
-
|
Changing the endpoint from Edit: this issue persists in the current version 0.77.1 |
Beta Was this translation helpful? Give feedback.
-
|
Closing the connection seems to help in all of the mentioned FastAPI versions (at least on my machine). Edit: This does not require the endpoints to be before@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
conn = ENGINE.connect()
stmt = select([T]).with_for_update().where(T.c.id == job_id)
row = conn.execute(stmt).fetchone()
if row is None:
# This exception that is raised in the first test causes the second test to
# hang during its setup.
# If we do "return None" instead, the second test will be run.
# return None
raise HTTPException(404, f'Job with ID "{job_id}" not found')
return rowafter@app.get("/jobs/{job_id}")
def get_job(job_id: int = Path(...)):
with ENGINE.connect() as conn:
stmt = select([T]).with_for_update().where(T.c.id == job_id)
row = conn.execute(stmt).fetchone()
if row is None:
# This exception that is raised in the first test causes the second test to
# hang during its setup.
# If we do "return None" instead, the second test will be run.
# return None
raise HTTPException(404, f'Job with ID "{job_id}" not found')
return row |
Beta Was this translation helpful? Give feedback.
-
|
Yup, that solves it for me. Thx. :) |
Beta Was this translation helpful? Give feedback.
Closing the connection seems to help in all of the mentioned FastAPI versions (at least on my machine).
Edit: This does not require the endpoints to be
async deffunctions.before
after
@