testserver.test_server
testserver.test_server(app=None, *, client_data=None, timeout_secs=5.0)Run a Shiny server function, Express app, or shiny.App in memory for testing.
The Python counterpart to R Shiny's testServer(). The app's server function runs against a mock connection, so there is no browser and no network server: set inputs, let the reactive graph settle, and assert on outputs — all in process, in an ordinary (non-async) test.
app accepts every way of naming what to test, and defaults to "app.py" next to the test file, like the local_app fixture. The returned session must be used as a context manager, which guarantees the app is torn down even when an assertion fails.
In an async test, use test_server_async instead — this function drives its own event loop and cannot run inside a loop that 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(). 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 |
|---|---|---|
| TestServerSession | An unstarted TestServerSession, to be used with with. |
Notes
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() # app.py beside the test file
test_server("myapp.py") # another file beside the test file
test_server(path_to_app) # absolute Path, used as-is
test_server(my_mod_server) # server function, or a shiny.AppThe with block guarantees the app is torn down even when an assertion fails:
from shiny.testserver import test_server
def test_doubling_app():
with test_server("myapp.py") as ts:
ts.set_inputs(n=10)
assert ts.get_output("doubled") == "20"For the common case – app.py next to the test file – the built-in local_server pytest fixture is that block already entered, so a test body is nothing but interactions and assertions. It is what the rest of these examples use. Set inputs to simulate a user interacting, and read outputs between them. get_output compares equal to the value itself, so assertions need no unwrapping:
def test_doubling_app(local_server):
# Several inputs at once.
local_server.set_inputs(name="Ada", n=10)
assert local_server.is_ok
assert local_server.get_output("greeting") == "Hello, Ada!"
assert local_server.get_output("doubled") == "20"
# Values registered with `export_test_values()` are read the same way.
assert local_server.get_export("running_total") == 20
# A later interaction re-renders. Inputs you do not name keep their
# values, so `name` is still "Ada" here.
local_server.set_inputs(n=21)
assert local_server.get_output("doubled") == "42"local_server is function-scoped – each test gets a fresh session, since a session holds the inputs set so far – and it takes another app file through an indirect parametrization:
import pytest
@pytest.mark.parametrize("local_server", ["other_app.py"], indirect=True)
def test_the_other_app(local_server):
local_server.set_inputs(n=10)
assert local_server.get_output("tripled") == "30"set_inputs and flush both return what they were called on, so a sequence of interactions can be written as one chain:
def test_a_sequence_of_interactions(local_server):
assert (
local_server.set_inputs(name="Ada").set_inputs(n=10).get_output("doubled")
== "20"
)Each value also says how it turned out, which is what to inspect when an assertion is not simply about equality:
def test_reports_a_bad_value(local_server):
local_server.set_inputs(n=-1)
assert local_server.is_ok is False
failed = local_server.get_output("doubled")
assert failed.status == "error"
assert "must be positive" in failed.error
assert "raise ValueError" in failed.tracebackClient data has defaults, so a plot renders without a browser. Change one output’s size partway through a test by setting its .clientdata_* input, or set every output’s size up front with client_data= – an argument only test_server() takes, so that one is written with the with block:
def test_plot_at_a_given_size(local_server):
local_server.set_inputs(**{".clientdata_output_plot_width": 300})
assert local_server.get_output("plot").status == "ok"
def test_every_plot_at_a_given_size():
with test_server("myapp.py", client_data={"output_width": 300}) as ts:
assert ts.get_output("plot").status == "ok"A Shiny module namespaces its ids, so reach them through the id the module was given:
def test_counter_module(local_server):
local_server.set_inputs(**{"counter-n": 7})
assert local_server.get_output("counter-label") == "n=7"Or take a scope and use the bare ids the module’s own code uses, the way shiny.Session.make_scope hands a module its namespaced session:
def test_counter_module_in_scope(local_server):
counter = local_server.make_scope("counter")
counter.set_inputs(n=7)
assert counter.get_output("label") == "n=7"
# Scoped all the way down: only this module's items, keyed bare.
assert set(counter.to_values().outputs) == {"label"}A scope holds no state of its own, so take as many as the test needs:
def test_two_counters(local_server):
first = local_server.make_scope("first")
first.set_inputs(n=1)
assert first.get_output("label") == "n=1"
second = local_server.make_scope("second")
second.set_inputs(n=2)
assert second.get_output("label") == "n=2"Express modules namespace their ids the same way, so an Express app holding counter("counter") is reached with the very same ids, and by the same scope.
Nested modules compose their namespaces, so the id is every ancestor id joined by - – local_server.get_output("outer-inner-label") – or, as scopes, local_server.make_scope("outer").make_scope("inner").get_output("label").
A module’s server function can be tested on its own, with no app file at all: test_server() wraps it in an app with an empty UI. Here the fixture is one you write, since local_server only loads files. Keep it function-scoped – the default – for the same reason local_server is:
import pytest
from shiny import Inputs, Outputs, Session, module, render
from shiny.testserver import test_server
@module.server
def counter_server(input: Inputs, output: Outputs, session: Session):
@render.text
def label():
return f"n={input.n()}"
def app_server(input: Inputs, output: Outputs, session: Session):
counter_server("counter")
@pytest.fixture
def ts():
with test_server(app_server) as session:
yield session
def test_counter_module(ts):
ts.set_inputs(**{"counter-n": 7})
assert ts.get_output("counter-label") == "n=7"To assert after the app is gone, capture the values first. Both forms hold copies, so they stay valid once the with block has closed:
def test_reports_everything():
with test_server("myapp.py") as ts:
ts.set_inputs(n=10)
values = ts.to_values() # rich `TestServerValue`s
as_dict = dict(ts) # the same, as plain JSON-ready data
assert values.outputs["doubled"].value == "20"
assert as_dict["outputs"]["doubled"]["value"] == "20"