Testing

Slide Contents


title: “Testing Shiny Apps”

Why test?

  • Manual “click around and check” doesn’t scale
  • Catch regressions before users do
  • Document expected behavior as executable code

Visual check + assert statements

def my_add(x, y):
    return x + y
my_add(3, 10)
13
assert my_add(3, 10) == 13
assert my_add(3, 10) == 100
---------------------------------------------------------------------------
AssertionError                            Traceback (most recent call last)
Cell In[4], line 1
----> 1 assert my_add(3, 10) == 100

AssertionError: 

Unit testing - pytest

  • Need to know a bit of how packages are set up

Python functions + Shiny

  • If you have a normal python function, you can still pass in reactive values as the arguments!
    • Import your existing functions / modules
    • Use other packages
  • As long as you don’t plan on having reactive calls inside the function body
    • Remember: reactive things must happen inside a reactive context
  • pytest is the package / tool to do unit testing

Playwright - end-to-end testing

Playwright is a modern browser automation library.

Shiny provides a Playwright controller that understands Shiny-specific widgets:

from playwright.sync_api import Page
from shiny.playwright import controller
from shiny.run import ShinyAppProc

Docs: https://shiny.posit.co/py/docs/testing.html

Test structure

from pathlib import Path
from playwright.sync_api import Page
from shiny.playwright import controller
from shiny.run import run_shiny_app

APP = Path(__file__).parent / "app.py"

def test_species_filter(page: Page):
    with run_shiny_app(str(APP)) as app:
        page.goto(app.url)

        radio = controller.InputRadioButtons(page, "species")
        text  = controller.OutputText(page, "n_rows")

        radio.expect_choices(["Adelie", "Chinstrap", "Gentoo"])
        radio.set("Chinstrap")
        text.expect_value("68 penguins")

Running tests

code/demos/testing/
├── app.py          ← the Shiny app
└── test_app.py     ← tests
cd code/demos/testing
pytest

Or from the repo root:

pytest code/demos/testing/
Tip🎯 Live Demo

Run pytest code/demos/testing/ in the terminal.

Controllers

Shiny provides typed controllers for every input and output type:

# Inputs
controller.InputRadioButtons(page, "id")
controller.InputSlider(page, "id")
controller.InputSelect(page, "id")
controller.InputCheckboxGroup(page, "id")

# Outputs
controller.OutputPlot(page, "id")
controller.OutputText(page, "id")
controller.OutputDataFrame(page, "id")

What to test

  • Default state renders without errors
  • Each input change produces the expected output change
  • Edge cases: empty selections, out-of-range values
  • Error messages appear when they should

Exercise

See exercise: Testing.

Code

code/demos/testing/app.py

from palmerpenguins import load_penguins
from shiny.express import input, render, ui

dat = load_penguins().dropna()

ui.input_radio_buttons("species", "Species", ["Adelie", "Chinstrap", "Gentoo"])

with ui.card():
    @render.text
    def n_rows():
        filtered = dat[dat["species"] == input.species()]
        return f"{len(filtered)} penguins"

code/demos/testing/test_app.py

from pathlib import Path

from playwright.sync_api import Page
from shiny.playwright import controller
from shiny.run import run_shiny_app

APP = Path(__file__).parent / "app.py"


def test_default_state(page: Page):
    with run_shiny_app(str(APP)) as app:
        page.goto(app.url)

        radio = controller.InputRadioButtons(page, "species")
        radio.expect_selected("Adelie")
        radio.expect_choices(["Adelie", "Chinstrap", "Gentoo"])


def test_species_filter(page: Page):
    with run_shiny_app(str(APP)) as app:
        page.goto(app.url)

        radio = controller.InputRadioButtons(page, "species")
        text = controller.OutputText(page, "n_rows")

        radio.set("Chinstrap")
        text.expect_value("68 penguins")