Skip to content

Test Client

Ravyn 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 ravyn testing suite to be installed. You can do it so by running:

$ pip install ravyn[test]

API Reference

Check the API Reference for the test client to understand more.

The test client

from ravyn.testclient import RavynTestClient
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 = RavynTestClient(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 Ravyn 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 ravyn.testclient import RavynTestClient
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 = RavynTestClient(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 RavynTestClient.

from ravyn.testclient import RavynTestClient
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 = RavynTestClient(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 RavynTestClient 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 = RavynTestClient(app, raise_server_exceptions=False).

Lifespan events

Note

Ravyn supports all the lifespan events available and therefore on_startup, on_shutdown and lifespan are also supported by RavynTestClient but if you need to test these you will need to run RavynTestClient as a context manager or otherwise the events will not be triggered when the RavynTestClient 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 ravyn import Gateway, Include, Request, WebSocket, WebSocketGateway, get, websocket
from ravyn.utils.enums import MediaType
from ravyn.permissions import AllowAny, DenyAll
from ravyn.responses import JSONResponse
from ravyn.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 ravyn.testclient import override_settings

Let us see an example.

from lilya.middleware import DefineMiddleware

from ravyn import Ravyn, Gateway, get
from ravyn.middleware.clickjacking import XFrameOptionsMiddleware
from ravyn.responses import PlainText
from ravyn.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 = Ravyn(
        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 ravyn import Ravyn, Gateway, get
from ravyn.middleware.clickjacking import XFrameOptionsMiddleware
from ravyn.responses import PlainText
from ravyn.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"