Test Client¶
Esmerald 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 esmerald testing suite to be installed. You can do it so by running:
$ pip install esmerald[test]
API Reference¶
Check the API Reference for the test client to understand more.
The test client¶
from esmerald.testclient import EsmeraldTestClient
from lilya.responses import HTMLResponse
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 = EsmeraldTestClient(app)
response = client.get("/")
assert response.status_code == 200
The test client is very similar to its original as it extends it and adds extra unique and specifics for Esmerald
and therefore the same examples and use cases will work.
You can use any of the httpx
standard API like authentication, session cookies and file uploads.
from esmerald.testclient import EsmeraldTestClient
from lilya.responses import HTMLResponse
async def app(scope, receive, send):
assert scope["type"] == "http"
response = HTMLResponse("<html><body>Hello, world!</body></html>")
await response(scope, receive, send)
client = EsmeraldTestClient(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": "..."})
And like Lilya, the same example to send files with EsmeraldTestClient
.
from esmerald.testclient import EsmeraldTestClient
from lilya.responses import HTMLResponse
async def app(scope, receive, send):
assert scope["type"] == "http"
response = HTMLResponse("<html><body>Hello, world!</body></html>")
await response(scope, receive, send)
client = EsmeraldTestClient(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 Django Rest Framework
.
Info
By default the EsmeraldTestClient 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 = EsmeraldTestClient(app, raise_server_exceptions=False)
.
Lifespan events¶
Note
Esmerald supports all the lifespan events available and therefore on_startup
, on_shutdown
and lifespan
are
also supported by EsmeraldTestClient
but if you need to test these you will need to run EsmeraldTestClient
as a context manager or otherwise the events will not be triggered when the EsmeraldTestClient
is instantiated.
The framework also brings a ready to use functionality to be used as context manager for your tests.
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 esmerald import Gateway, Include, Request, WebSocket, WebSocketGateway, get, websocket
from esmerald.enums import MediaType
from esmerald.permissions import AllowAny, DenyAll
from esmerald.responses import JSONResponse
from esmerald.testclient import create_client
from lilya.responses import Response
@get(path="/", permissions=[DenyAll])
async def deny_access(request: Request) -> JSONResponse:
return JSONResponse("Hello, world")
@get(path="/", permissions=[AllowAny])
async def allow_access(request: Request) -> JSONResponse:
return JSONResponse("Hello, world")
@get(path="/", media_type=MediaType.TEXT, status_code=200)
async def homepage(request: Request) -> Response:
return Response("Hello, world")
@websocket(path="/")
async def websocket_endpoint(socket: WebSocket) -> None:
await socket.accept()
await socket.send_text("Hello, world!")
await socket.close()
routes = [
Gateway("/", handler=homepage, name="homepage"),
Include(
"/nested",
routes=[
Include(
path="/test/",
routes=[Gateway(path="/", handler=homepage, name="nested")],
),
Include(
path="/another",
routes=[
Include(
path="/test",
routes=[Gateway(path="/{param}", handler=homepage, name="nested")],
)
],
),
],
),
Include(
"/static",
app=Response("xxxxx", media_type=MediaType.PNG, status_code=200),
),
WebSocketGateway("/ws", handler=websocket_endpoint, name="websocket_endpoint"),
Gateway("/deny", handler=deny_access, name="deny_access"),
Gateway("/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 esmerald.testclient import override_settings
Let us see an example.
from lilya.middleware import DefineMiddleware
from esmerald import Esmerald, Gateway, get
from esmerald.middleware.clickjacking import XFrameOptionsMiddleware
from esmerald.responses import PlainText
from esmerald.testclient import override_settings
@override_settings(x_frame_options="SAMEORIGIN")
def test_xframe_options_same_origin_responses(test_client_factory):
@get()
def homepage() -> PlainText:
return PlainText("Ok", status_code=200)
app = Esmerald(
routes=[Gateway("/", 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.middleware import DefineMiddleware
from esmerald import Esmerald, Gateway, get
from esmerald.middleware.clickjacking import XFrameOptionsMiddleware
from esmerald.responses import PlainText
from esmerald.testclient import override_settings
def test_xframe_options_same_origin_responses(test_client_factory):
@get()
def homepage() -> PlainText:
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"