testserver.test_server_async

testserver.test_server_async(app=None, *, client_data=None, timeout_secs=5.0)

The async counterpart to test_server, for use in async tests.

Behaves like test_server but yields an AsyncTestServerSession, whose set_inputs must be awaited. Use this whenever the test itself is async (for example under @pytest.mark.asyncio) — the synchronous test_server drives its own event loop and will raise if one is already running.

Parameters

Name Type Description Default
app Optional[Union[App, Callable[…, Any], str, Path]] What to test, defaulting to "app.py": * A server function, which is wrapped in an app with an empty UI. * A shiny.App instance. * A path to an app file, Core or Express. A str, or a Path that is not already a file, is resolved relative to the directory of the file calling test_server_async(). Pass a str to be sure a path stays relative. None
client_data Optional[Mapping[str, Any]] Stand-ins for what a browser would report, merged over DEFAULT_CLIENT_DATA. Keys are named after the readers on ClientData: an output_* key applies to every output, and every other key is session-wide. See the note below. None means the same as {}: every default is used. None
timeout_secs float How long to wait for any single reactive flush, including the initial one, before raising TimeoutError. Reactive code that blocks without awaiting holds the event loop and cannot be interrupted, so an overrun caused that way is reported once the flush finishes rather than partway through. 5.0

Returns

Name Type Description
AsyncTestServerSession An unstarted AsyncTestServerSession, to be used with async with.

Notes

NoteClient data

A real browser reports values back to the server: how big each output is, the device pixel ratio, and the parts of the URL. Your app reads them through session.clientdata, and renderers read them too — render.plot needs a width and height before it can draw anything.

There is no browser here, so those inputs would never arrive. A plot would render nothing and session.clientdata.url_pathname() would never resolve, both silently: the output would simply be "silent" and the session would still report success.

The session therefore sends the stand-ins in DEFAULT_CLIENT_DATA as soon as it starts, so these work out of the box. Override any of them with client_data=, and change one output’s size mid-test with set_inputs.

Examples

Every way of naming what to test:

test_server_async()                 # app.py beside the test file
test_server_async("myapp.py")       # another file beside the test file
test_server_async(path_to_app)      # absolute Path, used as-is
test_server_async(my_mod_server)    # server function, or a shiny.App

Set inputs to simulate a user interacting, and read outputs between them. Only set_inputs is awaited; reading a value is not:

import pytest

from shiny.testserver import test_server_async


@pytest.mark.asyncio
async def test_doubling_app():
    async with test_server_async("myapp.py") as ts:
        # Several inputs at once.
        await ts.set_inputs(name="Ada", n=10)

        assert ts.is_ok
        assert ts.get_output("greeting") == "Hello, Ada!"
        assert ts.get_output("doubled") == "20"

        # Values registered with `export_test_values()` are read the same way.
        assert ts.get_export("running_total") == 20

        # A later interaction re-renders. Inputs you do not name keep their
        # values, so `name` is still "Ada" here.
        await ts.set_inputs(n=21)
        assert ts.get_output("doubled") == "42"

Each value also says how it turned out, which is what to inspect when an assertion is not simply about equality:

@pytest.mark.asyncio
async def test_reports_a_bad_value():
    async with test_server_async("myapp.py") as ts:
        await ts.set_inputs(n=-1)

        assert ts.is_ok is False
        failed = ts.get_output("doubled")
        assert failed.status == "error"
        assert "must be positive" in failed.error
        assert "raise ValueError" in failed.traceback

Client data has defaults, so a plot renders without a browser. Override them when a test cares about the size:

@pytest.mark.asyncio
async def test_plot_at_a_given_size():
    async with test_server_async(
        "myapp.py", client_data={"output_width": 300}
    ) as ts:
        assert ts.get_output("plot").status == "ok"

The remaining patterns are the same as for test_server – testing a module through its namespaced ids, wrapping the session in a fixture, and capturing values that outlive the block – except that set_inputs is awaited.

See Also