Ir para o conteúdo

Cliente de Teste

O Lilya vem com um cliente de teste para os testes da sua aplicação. Não é obrigatório usá-lo, pois cada aplicação e equipa de desenvolvimento tem sua própria maneira de testar, mas caso precise, está disponível.

Requisitos

Esta secção requer que a suíte de testes do Lilya esteja instalada. Pode fazer isso executando:

$ pip install Lilya[test]

O cliente de teste

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

Pode usar qualquer uma das APIs padrão do httpx, como autenticação, cookies de sessão e envio de ficheiros.

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 é uma ótima biblioteca criada pelo mesmo autor do Starlette e do Django Rest Framework.

Info

Por defeito, o TestClient lança qualquer exceção que ocorra na aplicação. Ocasionalmente, pode querer testar o conteúdo das respostas de erros 500, em vez de permitir que o cliente lance a exceção do servidor. Nesse caso, deve utilizar client = TestClient(app, raise_server_exceptions=False).

Eventos de ciclo de vida

Note

O Lilya suporta todos os eventos de ciclo de vida disponíveis e, portanto, on_startup, on_shutdown e lifespan também são suportados pelo TestClient mas se precisar testá-los, precisará executar o TestClient como um gestor de contexto, caso contrário, os eventos não serão acionados quando o TestClient for instanciado.

O Lilya também traz uma funcionalidade pronta a usar que pode ser utilizada como um gestor de contexto para testes, o create_client.

Gestor de contexto create_client

Esta função está preparada para ser utilizada como um gestor de contexto para testes e está pronta para ser utilizada a qualquer momento.

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!"

Os testes funcionam tanto com funções sync quanto async.

Info

O exemplo acima também é usado para mostrar que os testes podem ser tão complexos quanto desejar e funcionarão com o gestor de contexto.

override_settings

Este é um decorator especial do Lilya e serve como auxiliar para os testes quando precisa atualizar/mudar as configurações temporariamente para testar qualquer cenário que exija valores específicos para as configurações terem valores diferentes.

O override_settings atua como um decorator normal ou como um gestor de contexto.

As configurações que pode substituir são as declaradas nas configurações.

from lilya.testclient import override_settings

Vejamos um exemplo.

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"

Ou como gestor de contexto.

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"