Testing ======= Testing a Litestar application is made simple by the testing utilities provided out of the box. Based on `httpx `_, they come with a familiar interface and integrate seamlessly into synchronous or asynchronous tests. Test Clients ------------ Litestar provides 2 test clients: - :class:`~litestar.testing.AsyncTestClient`: An asynchronous test client to be used in asynchronous environments. It runs the application and client on an externally managed event loop. Ideal for testing asynchronous behaviour, or when dealing with asynchronous resources - :class:`~litestar.testing.TestClient`: A synchronous test client. It runs the application in a newly created event loop within a separate thread. Ideal when no async behaviour needs to be tested, and no external event loop is provided by the testing library Let's say we have a very simple app with a health check endpoint: .. code-block:: python :caption: ``my_app/main.py`` from litestar import Litestar, MediaType, get @get(path="/health-check", media_type=MediaType.TEXT) def health_check() -> str: return "healthy" app = Litestar(route_handlers=[health_check]) We would then test it using the test client like so: .. tab-set:: .. tab-item:: Sync :sync: sync .. code-block:: python :caption: ``tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import TestClient from my_app.main import app app.debug = True def test_health_check(): with TestClient(app=app) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" .. tab-item:: Async :sync: async .. code-block:: python :caption: ``tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import AsyncTestClient from my_app.main import app app.debug = True async def test_health_check(): async with AsyncTestClient(app=app) as client: response = await client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" Since we would probably need to use the client in multiple places, it's better to make it into a pytest fixture: .. tab-set:: .. tab-item:: Sync :sync: sync .. code-block:: python :caption: ``tests/conftest.py`` from typing import TYPE_CHECKING, Iterator import pytest from litestar.testing import TestClient from my_app.main import app if TYPE_CHECKING: from litestar import Litestar app.debug = True @pytest.fixture(scope="function") def test_client() -> Iterator[TestClient[Litestar]]: with TestClient(app=app) as client: yield client .. tab-item:: Async :sync: async .. code-block:: python :caption: ``tests/conftest.py`` from typing import TYPE_CHECKING, AsyncIterator import pytest from litestar.testing import AsyncTestClient from my_app.main import app if TYPE_CHECKING: from litestar import Litestar app.debug = True @pytest.fixture(scope="function") async def test_client() -> AsyncIterator[AsyncTestClient[Litestar]]: async with AsyncTestClient(app=app) as client: yield client We would then be able to rewrite our test like so: .. tab-set:: .. tab-item:: Sync :sync: sync .. literalinclude:: /examples/testing/test_health_check_sync.py :caption: ``tests/test_health_check.py`` :language: python .. tab-item:: Async :sync: async .. literalinclude:: /examples/testing/test_health_check_async.py :caption: ``tests/test_health_check.py`` :language: python Deciding which test client to use +++++++++++++++++++++++++++++++++ In most situations, it doesn't make a functional difference, and just comes down to preference, as both clients offer the same API and capabilities. However, there are some situations where the way the clients run and interact with the application are important, specifically when testing in an asynchronous context. A common issue when using `anyio's pytest plugin `_ or `pytest-asyncio `_ to run asynchronous tests or fixtures, using the synchronous :class:`~litestar.testing.TestClient` means that the application will run in a *different event loop* than the test or fixture. In practice, this can result in some difficult to debug and solve situations, especially when setting up async resources outside the application, for example when using the factory pattern. The following example uses a shared instance of an ``httpx.AsyncClient``. It uses the common factory function, which allows to customise the client for tests, for example to add authentication headers. .. literalinclude:: /examples/testing/async_resource_test_issue.py :language: python Running this test will fail with a ``RuntimeError: Event loop is closed``, when trying to close the ``AsyncClient`` instance. This is happening because: - The ``http_test_client`` fixture sets up the client in *event loop A* - The ``TestClient`` instance created within the ``test_handler`` test sets up *event loop B* and runs the application in it - A call to ``http_client.get``, the ``httpx.AsyncClient`` instance creates a new connection within *loop B* and attaches it to the client instance - The ``TestClient`` instance closes *event loop B* - The cleanup step of the ``http_test_client`` fixture calls ``httpx.AsyncClient.aclose()`` instance within *loop A*, which internally tries to close the connection made in the previous step. That connection however is still attached to *loop B* that was owned by the ``TestClient`` instance, and is now closed This can easily fixed by switching the test from :class:`~litestar.testing.TestClient` to :class:`~litestar.testing.AsyncTestClient`: .. literalinclude:: /examples/testing/async_resource_test_issue_fix.py :language: python Now the fixture, test and application code are all running within the same event loop, ensuring that all resources can be cleaned up properly without issues. .. literalinclude:: /examples/testing/event_loop_demonstration.py :language: python :caption: Showcasing the different running event loops when using ``TestClient`` Testing websockets ++++++++++++++++++ Litestar's test client enhances the httpx client to support websockets. To test a websocket endpoint, you can use the :meth:`websocket_connect ` method on the test client. The method returns a websocket connection object that you can use to send and receive messages, see an example below for json: For more information, see also the :class:`WebSocket ` class in the API documentation and the :ref:`websocket ` documentation. .. tab-set:: .. tab-item:: Sync :sync: sync .. literalinclude:: /examples/testing/test_websocket_sync.py :language: python .. tab-item:: Async :sync: async .. literalinclude:: /examples/testing/test_websocket_async.py :language: python Using sessions ++++++++++++++ If you are using :ref:`session middleware ` for session persistence across requests, then you might want to inject or inspect session data outside a request. For this, :class:`TestClient <.testing.TestClient>` provides two methods: * :meth:`set_session_data ` * :meth:`get_session_data ` .. tab-set:: .. tab-item:: Sync :sync: sync .. literalinclude:: /examples/testing/test_set_session_data.py :caption: Setting session data :language: python .. literalinclude:: /examples/testing/test_get_session_data.py :caption: Getting session data :language: python .. tab-item:: Async :sync: async .. literalinclude:: /examples/testing/test_set_session_data_async.py :caption: Setting session data :language: python .. literalinclude:: /examples/testing/test_get_session_data_async.py :caption: Getting session data :language: python Running async functions on TestClient +++++++++++++++++++++++++++++++++++++ When using the synchronous :class:`TestClient <.testing.TestClient>`, it runs the application in a separate thread, which provides the event loop. For this, it makes use of :class:`anyio.BlockingPortal `. ``TestClient`` makes this portal public, so it can be used to run arbitrary asynchronous code in the same event loop as the application: .. literalinclude:: /examples/testing/test_with_portal.py :caption: Using a blocking portal :language: python Creating a test app ------------------- Litestar also offers a helper function called :func:`create_test_client ` which first creates an instance of Litestar and then a test client using it. There are multiple use cases for this helper - when you need to check generic logic that is decoupled from a specific Litestar app, or when you want to test endpoints in isolation. .. code-block:: python :caption: ``my_app/tests/test_health_check.py`` from litestar.status_codes import HTTP_200_OK from litestar.testing import create_test_client from my_app.main import health_check def test_health_check(): with create_test_client([health_check]) as client: response = client.get("/health-check") assert response.status_code == HTTP_200_OK assert response.text == "healthy" Running a live server --------------------- The test clients make use of HTTPX's ability to directly call into an ASGI app, without having to run an actual server. In most cases this is sufficient but there are some exceptions where this won't work, due to the limitations of the emulated client-server communication. For example, when using server-sent events with an infinite generator, it will lock up the test client, since HTTPX tries to consume the full response before returning a request. Litestar offers two helper functions, :func:`litestar.testing.subprocess_sync_client` and :func:`litestar.testing.subprocess_async_client` that will launch a Litestar instance with in a subprocess and set up an httpx client for running tests. You can either load your actual app file or create subsets from it as you would with the regular test client setup: .. literalinclude:: /examples/testing/subprocess_sse_app.py :language: python .. literalinclude:: /examples/testing/test_subprocess_sse.py :language: python By default, the subprocess client will capture all output from the litestar instance. To discard output in the main (testing) process, set the ``capture_output`` argument to ``False`` when creating the client: .. code-block:: python @pytest.fixture(name="async_client") async def fx_async_client() -> AsyncIterator[httpx.AsyncClient]: async with subprocess_async_client(workdir=ROOT, app="subprocess_sse_app:app", capture_output=False) as client: yield client RequestFactory -------------- Another helper is the :class:`RequestFactory ` class, which creates instances of :class:`litestar.connection.request.Request `. The use case for this helper is when you need to test logic that expects to receive a request object. For example, lets say we wanted to unit test a *guard* function in isolation, to which end we'll reuse the examples from the :doc:`route guards ` documentation: .. code-block:: python :caption: ``my_app/guards.py`` from litestar import Request from litestar.exceptions import NotAuthorizedException from litestar.handlers.base import BaseRouteHandler def secret_token_guard(request: Request, route_handler: BaseRouteHandler) -> None: if ( route_handler.opt.get("secret") and not request.headers.get("Secret-Header", "") == route_handler.opt["secret"] ): raise NotAuthorizedException() We already have our route handler in place: .. code-block:: python :caption: ``my_app/secret.py`` from os import environ from litestar import get from my_app.guards import secret_token_guard @get(path="/secret", guards=[secret_token_guard], opt={"secret": environ.get("SECRET")}) def secret_endpoint() -> None: ... We could thus test the guard function like so: .. code-block:: python :caption: ``tests/guards/test_secret_token_guard.py`` import pytest from litestar.exceptions import NotAuthorizedException from litestar.testing import RequestFactory from my_app.guards import secret_token_guard from my_app.secret import secret_endpoint request = RequestFactory().get("/") def test_secret_token_guard_failure_scenario(): copied_endpoint_handler = secret_endpoint.copy() copied_endpoint_handler.opt["secret"] = None with pytest.raises(NotAuthorizedException): secret_token_guard(request=request, route_handler=copied_endpoint_handler) def test_secret_token_guard_success_scenario(): copied_endpoint_handler = secret_endpoint.copy() copied_endpoint_handler.opt["secret"] = "super-secret" secret_token_guard(request=request, route_handler=copied_endpoint_handler)