Starting an app with Uvicorn for testing¶
Normally, testing of FastAPI apps should be done by passing the app to httpx.AsyncClient
and using HTTPX’s built-in support for sending requests directly to an ASGI application.
To do this, use the client
fixture provided by the fastapi_safir_app
template (see Creating an app from the template).
However, in some test scenarios it may be necessary for the app being tested to respond to regular HTTP requests, such as for Selenium testing with a browser. Testing integration with Uvicorn also requires running the app with Uvicorn.
safir.testing.uvicorn.spawn_uvicorn
is a helper function to spawn a separate Uvicorn process to run a test FastAPI application.
The details about the running process are provided in a returned dataclass, UvicornProcess
.
To use this function, write a fixture similar to the following.
This code assumes the source code of the test app is in the variable _APP_SOURCE
, and that code declares a variable named app
to hold the FastAPI object.
from collections.abc import Iterator
from safir.testing.uvicorn import UvicornProcess, spawn_uvicorn
@pytest.fixture
def server(tmp_path: Path) -> Iterator[UvicornProcess]:
app_path = tmp_path / "test.py"
app_path.write_text(_APP_SOURCE)
uvicorn = spawn_uvicorn(working_directory=tmp_path, app="test:app")
yield uvicorn
uvicorn.process.terminate()
The .url
attribute of the returned object will contain the base URL of the running app.
It will be listening on localhost on a random high-numbered port.
Writing out small test files is useful for quick tests.
For more complex applications, the app
argument can instead be any variable in any module on the Python search path that contains a FastAPI app to run.
Additional environment variables for the app can be passed via the env
argument to spawn_uvicorn
.
Alternately, if you need to dynamically create the application (using other fixtures, for example), you can pass in a factory function:
from collections.abc import Iterator
from safir.testing.uvicorn import UvicornProcess, spawn_uvicorn
@pytest.fixture
def server(tmp_path: Path) -> Iterator[UvicornProcess]:
app_path = tmp_path / "test.py"
app_path.write_text(_APP_SOURCE)
uvicorn = spawn_uvicorn(
working_directory=tmp_path,
factory="tests.support.selenium:create_app",
)
yield uvicorn
uvicorn.process.terminate()
By default, the output from Uvicorn is sent to the normal standard output and standard error, where it will be captured by pytest but will not be available to the test.
If the test itself needs to inspect the Uvicorn output, pass capture=True
to spawn_uvicorn
, and then call the communicate
method on the .process
attribute of the returned object to retrieve the standard output and standard error (generally after calling its terminate
method).
For example:
import pytest
from httpx import AsyncClient
from safir.testing.uvicorn import UvicornProcess, spawn_uvicorn
@pytest.mark.asyncio
def test_something(tmp_path: Path) -> None:
app_path = tmp_path / "test.py"
app_path.write_text(_APP_SOURCE)
uvicorn = spawn_uvicorn(
working_directory=tmp_path,
factory="tests.support.selenium:create_app",
capture=True,
)
try:
async with AsyncClient() as client:
# Interact with app at uvicorn.url
...
finally:
uvicorn.process.terminate()
stdout, stderr = uvicorn.process.communicate()
# Do something with stdout and stderr