Test Client¶
Lilya comes with a test client for your application tests. It is not mandatory use it as every application and development team has its own way of testing it but just in case, it is provided.
Requirements¶
This section requires the Lilya testing suite to be installed. You can do it so by running:
$ pip install Lilya[test]
The test client¶
from lilya.responses import HTMLResponse
from lilya.testclient import TestClient
async def app(scope, receive, send):
assert scope["type"] == "http"
response = HTMLResponse("<html><body>Hello, world!</body></html>")
await response(scope, receive, send)
def test_application():
client = TestClient(app)
response = client.get("/")
assert response.status_code == 200
You can use any of the httpx
standard API like authentication, session cookies and file uploads.
from lilya.responses import HTMLResponse
from lilya.testclient import TestClient
async def app(scope, receive, send):
assert scope["type"] == "http"
response = HTMLResponse("<html><body>Hello, world!</body></html>")
await response(scope, receive, send)
client = TestClient(app)
# Set headers on the client for future requests
client.headers = {"Authorization": "..."}
response = client.get("/")
# Set headers for each request separately
response = client.get("/", headers={"Authorization": "..."})
TestClient
from lilya.responses import HTMLResponse
from lilya.testclient import TestClient
async def app(scope, receive, send):
assert scope["type"] == "http"
response = HTMLResponse("<html><body>Hello, world!</body></html>")
await response(scope, receive, send)
client = TestClient(app)
# Send a single file
with open("example.txt", "rb") as f:
response = client.post("/form", files={"file": f})
# Send multiple files
with open("example.txt", "rb") as f1:
with open("example.png", "rb") as f2:
files = {"file1": f1, "file2": ("filename", f2, "image/png")}
response = client.post("/form", files=files)
httpx
is a great library created by the same author of Starlette
and Django Rest Framework
.
Info
By default the TestClient raise any exceptions that occur in the application.
Occasionally you might want to test the content of 500 error responses, rather than allowing client to raise the
server exception. In this case you should use client = TestClient(app, raise_server_exceptions=False)
.
Lifespan events¶
Note
Lilya supports all the lifespan events available and therefore on_startup
, on_shutdown
and lifespan
are
also supported by TestClient
but if you need to test these you will need to run TestClient
as a context manager or otherwise the events will not be triggered when the TestClient
is instantiated.
Lilya also brings a ready to use functionality to be used as context manager for your tests, the create_client
.
Context manager create_client
¶
This function is prepared to be used as a context manager for your tests and ready to use at any given time.
import pytest
from lilya.enums import MediaType
from lilya.responses import JSONResponse, Response
from lilya.routing import Include, Path, WebSocketPath
from lilya.testclient import create_client
from lilya.websockets import WebSocket
async def allow_access():
return JSONResponse("Hello, world")
async def homepage():
return Response("Hello, world")
async def websocket_endpoint(websocket: WebSocket):
await websocket.accept()
await websocket.send_text("Hello, world!")
await websocket.close()
routes = [
Path("/", handler=homepage, name="homepage"),
Include(
"/nested",
routes=[
Include(
path="/test/",
routes=[Path(path="/", handler=homepage, name="nested")],
),
Include(
path="/another",
routes=[
Include(
path="/test",
routes=[Path(path="/{param}", handler=homepage, name="nested")],
)
],
),
],
),
Include(
"/static",
app=Response("xxxxx", media_type=MediaType.PNG, status_code=200),
),
WebSocketPath("/ws", handler=websocket_endpoint, name="websocket_endpoint"),
Path("/allow", handler=allow_access, name="allow_access"),
]
@pytest.mark.filterwarnings(
r"ignore"
r":Trying to detect encoding from a tiny portion of \(5\) byte\(s\)\."
r":UserWarning"
r":charset_normalizer.api"
)
def test_router():
with create_client(routes=routes) as client:
response = client.get("/")
assert response.status_code == 200
assert response.text == "Hello, world"
response = client.post("/")
assert response.status_code == 405
assert response.json()["detail"] == "Method POST not allowed."
assert response.headers["content-type"] == MediaType.JSON
response = client.get("/foo")
assert response.status_code == 404
assert response.json()["detail"] == "The resource cannot be found."
response = client.get("/static/123")
assert response.status_code == 200
assert response.text == "xxxxx"
response = client.get("/nested/test")
assert response.status_code == 200
assert response.text == "Hello, world"
response = client.get("/nested/another/test/fluid")
assert response.status_code == 200
assert response.text == "Hello, world"
with client.websocket_connect("/ws") as session:
text = session.receive_text()
assert text == "Hello, world!"
The tests work with both sync
and async
functions.
Info
The example above is used to also show the tests can be as complex as you desire and it will work with the context manager.
override_settings¶
This is a special decorator from Lilya and serves as the helper for your tests when you need to update/change the settings for a given test temporarily to test any scenario that requires specific settings to have different values.
The override_settings
acts as a normal function decorator or as a context manager.
The settings you can override are the ones declared in the settings.
from lilya.testclient import override_settings
Let us see an example.
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.clickjacking import XFrameOptionsMiddleware
from lilya.responses import PlainText
from lilya.routing import Path
from lilya.testclient.utils import override_settings
@override_settings(x_frame_options="SAMEORIGIN")
def test_xframe_options_same_origin_responses(test_client_factory):
def homepage():
return PlainText("Ok", status_code=200)
app = Lilya(
routes=[Path("/", handler=homepage)],
middleware=[DefineMiddleware(XFrameOptionsMiddleware)],
)
client = test_client_factory(app)
response = client.get("/")
assert response.headers["x-frame-options"] == "SAMEORIGIN"
Or as context manager.
from lilya.apps import Lilya
from lilya.middleware import DefineMiddleware
from lilya.middleware.clickjacking import XFrameOptionsMiddleware
from lilya.responses import PlainText
from lilya.routing import Path
from lilya.testclient.utils import override_settings
def test_xframe_options_same_origin_responses(test_client_factory):
def homepage():
return PlainText("Ok", status_code=200)
with override_settings(x_frame_options="SAMEORIGIN"):
app = Lilya(
routes=[Path("/", handler=homepage)],
middleware=[DefineMiddleware(XFrameOptionsMiddleware)],
)
client = test_client_factory(app)
response = client.get("/")
assert response.headers["x-frame-options"] == "SAMEORIGIN"