Skip to content

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"