This directory contains examples of how to properly run and stop uvicorn servers for integration testing. Unlike the problematic approach of using uvicorn.run() (which blocks forever), these examples show you how to create real HTTP endpoints that you can send requests to.
The original code had this issue:
@pytest.mark.asyncio
async def test_basic():
uvicorn.run(app, host="127.0.0.1", port=8000) # ❌ This blocks forever!
async with ClientSession() as session: # ❌ This never executes
# ... test code that never runsProblem: uvicorn.run() is a blocking call that never returns, so your test code after it never executes.
The UvicornTestServer class in test_basic.py provides a clean way to start and stop uvicorn servers:
from test_basic import UvicornTestServer
# Create and start server
server = UvicornTestServer(app, host="127.0.0.1", port=8000)
server.start()
# Make HTTP requests
async with ClientSession() as session:
async with session.get(f"{server.base_url}/health") as response:
assert response.status == 200
# Stop server
server.stop()Features:
- ✅ Automatic port detection (avoids conflicts)
- ✅ Proper startup/shutdown lifecycle
- ✅ Thread-based server execution
- ✅ Waits for server to be ready
- ✅ Graceful cleanup
Use pytest fixtures for automatic server management:
@pytest.fixture(scope="session")
def running_server():
"""Server shared across all tests in the session."""
server = UvicornTestServer(app)
server.start()
yield server
server.stop()
@pytest.fixture(scope="function")
def fresh_server():
"""Fresh server for each test."""
server = UvicornTestServer(app)
server.start()
yield server
server.stop()For full control over server lifecycle:
@pytest.mark.asyncio
async def test_manual_server():
server = UvicornTestServer(app)
try:
server.start()
# Your test code here
async with ClientSession() as session:
async with session.get(f"{server.base_url}/health") as response:
assert response.status == 200
finally:
server.stop() # Always cleanupcd integration-tests
uv sync# Run all tests
uv run python -m pytest test_basic.py -v
# Run specific test
uv run python -m pytest test_basic.py::test_basic_with_session_server -v -s
# Run with output
uv run python -m pytest test_basic.py -v -s# Start a server you can send requests to
uv run python demo_server.pyThen in another terminal:
curl http://127.0.0.1:8000/health
curl http://127.0.0.1:8000/testtest_basic.py- Main test file with UvicornTestServer class and examplestest_server_examples.py- Comprehensive examples of different testing approachesdemo_server.py- Simple script to run a server manuallypyproject.toml- Project dependencies
- ✅ Fast test execution (server starts once)
- ✅ Good for multiple tests that don't interfere
- ❌ Tests share state
- Use for: Most integration tests
- ✅ Complete isolation between tests
- ✅ Clean state for each test
- ❌ Slower (starts server for each test)
- Use for: Tests that modify server state
- ✅ Full control over lifecycle
- ✅ Can test server startup/shutdown
- ❌ More verbose
- Use for: Complex scenarios, debugging
- ✅ Perfect for development and debugging
- ✅ Can send real HTTP requests
- ✅ Easy to test endpoints manually
- Use for: Development, manual testing
- Automatic Port Detection: Finds free ports to avoid conflicts
- Proper Lifecycle: Clean startup and shutdown
- Thread Safety: Runs server in background thread
- Health Checking: Waits for server to be ready
- Graceful Shutdown: Proper cleanup on exit
- Error Handling: Robust error handling and timeouts
Once you have a running server, you can make requests using:
async with ClientSession() as session:
async with session.get(f"{server.base_url}/health") as response:
data = await response.json()
assert data["message"] == "OK"async with httpx.AsyncClient() as client:
response = await client.get(f"{server.base_url}/health")
assert response.status_code == 200curl http://127.0.0.1:8000/health
curl -X POST http://127.0.0.1:8000/api/data -H "Content-Type: application/json" -d '{"key": "value"}'import requests
response = requests.get(f"{server.base_url}/health")
assert response.status_code == 200Now you have a proper way to run uvicorn servers for integration testing that:
- ✅ Actually starts and stops properly
- ✅ Provides real HTTP endpoints
- ✅ Handles cleanup automatically
- ✅ Avoids port conflicts
- ✅ Works reliably in CI/CD
No more blocking uvicorn.run() calls or tests that never execute!