test: drive servers in memory with test_server() / testServer() - #304
Merged
Merged
Conversation
py-shiny#2470 added `shiny.testserver.test_server()`, the Python counterpart to R's `testServer()`. A ui.tsx server is only reactive computation, so "input X produces output Y" *is* the server, and both languages can now assert it with no browser. Package tests: - `pkg-py/tests/test_in_memory_server.py` — `reactive_output` publishing through a real session (`test_reactive_output.py` only reaches `Renderer.transform()`), module namespace isolation via `make_scope()`, and output error statuses. Reuses the Playwright fixture apps; needs no e2e extras, so it lives in the unit suite. - `pkg-r/tests/testthat/test-render.R` — the R mirrors of those two, with the divergence stated: R's `testServer()` re-raises where Python reports `.status`. Example tests, one per app with a Python server, plus R counterparts for the two that ship an `app.R` (07-plotly had no R test directory): - two `FEATURES.md` leaves were wrong and are now correct and tested: 05-temperature and 08-input-handler both claimed `input.x()` is `None` before the client's first message, so the server returns `None` / `"—"`. It raises a silent exception, so those branches are unreachable from a real client. - `(test)` markers added across seven example trees for the wire shapes, thresholds, pluralization, event handling, and `AsIs` wrapping these pin. Docs and skills: - the Quarto article and its R vignette mirror are retitled "Testing" and gain a server-testing half ahead of the wire-tap material. - `shinyreact-build-app/references/testing.md` said "`[py]` has no `testServer()` equivalent … This is a real gap"; it now documents `test_server()`, and `shinyreact-convert-app`'s verify phase goes from three layers to four. Shipped copies refreshed with `make update-skills`. - documented, because each cost time to find: pass an absolute `Path`; an untyped id needs no `:shinyreact.default` suffix but a typed id does; an event input needs two `set_inputs` calls; an unset input is `status == "silent"`, not a `None` value. `test_server()` is newer than shiny 1.7.0, which the docs and skills say explicitly. Two upstream issues filed from this work: posit-dev/py-shiny#2492 (a silent `req()` failure is invisible in memory — `get_output()` keeps the stale value while the browser blanks the output) and posit-dev/py-shiny#2493 (`status == "silent"` means "never rendered", inverting Shiny's own meaning). Drive-by: `test-examples.R` read every file in an example directory before filtering to `app.R` + `www/`, so `__pycache__` produced 208 `readLines()` warnings.
…70-testing # Conflicts: # pkg-r/tests/testthat/test-examples.R
Merging main brought in a new example. Its tests deliberately never load the app -- in Python that would trigger its build-on-first-run -- so test_server() is out of reach there, which is worth a leaf rather than a silent gap.
- 07-plotly: hoist skip_if_not_installed("plotly") to file scope, since
app.R calls plotly::renderPlotly() in server() and every testServer()
call needs it, not just the scatter test
- 08-input-handler: replace a tautological stdlib assertion with an
exact check on the echoed datetime; the handler decodes as UTC and
strips tzinfo, so the repr is machine-independent. Fix the FEATURES.md
leaf that claimed a tzinfo= in the repr.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
py-shiny#2470 added
shiny.testserver.test_server(), the Python counterpart to R'stestServer(): it runs an app's server against a mock connection, no browser and no subprocess.That suits shinyreact unusually well. A
ui.tsxserver contains only reactive computation, so "input X produces output Y" is the server, and the JSON a test asserts is exactly whatuseShinyOutputValue()receives:Package tests
pkg-py/tests/test_in_memory_server.py— three tests, reusing the Playwright fixture apps as-is:reactive_outputpublishing through a real session.test_reactive_output.pyonly reachesRenderer.transform(), so nothing pinned the wire path.ts.make_scope("a")— the in-memory twin ofplaywright/test_module_namespaces.py, which spends a browser on a purely server-side claim.playwright/test_output_error.py.It needs no
tests-e2eextras, so it belongs in the unit suite rather than the Playwright subtree.pkg-r/tests/testthat/test-render.R— the R mirrors of the last two, cross-referenced by name, with the divergence stated where it exists: R'stestServer()raises (expect_error(output$answer, class = "shiny.silent.error")) where Python's reports.status.Example tests
One Python test file per example with a server (01, 02, 05, 06, 07, 08, 10), plus R counterparts for the two that ship an
app.R— 07-plotly had no R test directory at all, so it gainstests/testthat.Rtoo.Two
FEATURES.mdleaves turned out to be wrong, and are now corrected and pinned: 05-temperature and 08-input-handler both claimedinput.x()isNonebefore the client's first message, so the server returnsNone/"—". It raises a silent exception — those branches are unreachable from a real client, and the outputs never render. That is the kind of claim only a test that drives the app can falsify, which is the argument for this whole PR in one leaf.(test)markers added across seven example trees for what these pin: wire shapes, zone thresholds, pluralization, the two-step event handling, theAsIswrapping that makes a one-bin R result a JSON array, and the plotly payload'splotly-main-but-not-plotly-bindingdependency split.examples/01-hello/tests/test_faithful.pyopened with "app.pycallsset_react_page()at module scope, so the two outputs' logic cannot be unit tested". No longer true anywhere in the tree.Docs and skills
pkg-py/docs/articles/testing.qmdand its R mirrorpkg-r/vignettes/articles/testing.Rmdare retitled Testing and gain a server-testing half ahead of the wire-tap material, tabbed py/R. Both render; headings and tabsets verified in the built HTML.shinyreact-build-app/references/testing.mdsaid "[py]has notestServer()equivalent … This is a real gap, not an oversight in your app." It now documentstest_server()alongside R's, andshinyreact-convert-app's verify phase goes from three layers to four.make update-skillsrun.Path(a relative one resolves against the test file's directory); an untyped id needs no:shinyreact.defaultsuffix (both Python handlers are no-ops) but a typed id does, since the suffix is what runs the handler; an event input needs twoset_inputscalls, mirroring the client's register-at-mount then send; and an unset input meansstatus == "silent", not aNonevalue.test_server()is newer than shiny 1.7.0 — which this repo already depends on via the py-shinymaingit dep — and the docs and skills say so explicitly.Two upstream issues filed from this work
req()failure is invisible in memory:get_output()keeps the previous value withstatus="ok"while the browser blanks the output, so a test can pass asserting a value the user cannot see. Includes R evidence thattestServer()does not retain the stale value, which is the parity rationale the current behavior cites.status == "silent"means "never rendered", the exact inversion of what Shiny already taughtreq()/silent to mean. Suggests"never-rendered", which also frees the name for #2492.Neither blocks this PR; the one leaf that would have asserted the missing behavior is written as a comment naming the issue instead.
Drive-by
pkg-r/tests/testthat/test-examples.Rread every file in an example directory and then filtered toapp.R+www/, so the new__pycache__produced 208readLines()warnings. It filters first now.Verification
uv run pytest— 195 passed;toxpy310 and py314 — 195 eachRscript -e 'testthat::test_local()'— 320 pass, 0 warn (was 208 warn)make r-check— 0 errors, 0 warnings, 0 notespytest,shiny::runTests()