This document covers the UI-related automated tests.
NOTE on work in progress / how to run the webapp tests
This package can use shinytest2::test_app().
EJAM:::test_ejam() instead uses an approach that first
sources testthat/setup.R which does setup needed and also
sources testthat/setup-shinytest2.R which defines
shinytest2_webapp_functionality(), a function that contains
the steps tested in the web app, and then directly uses that function
from within test files like
/tests/testthat/test-webapp-latlon-functionality.R etc.
Dev Environment
If you are successfully running the app, you should have all the
necessary packages, or at least those in Imports or Suggests of the
DESCRIPTION file. Some dev-related packages and tools to
note:
shinytest2 - shinytest2 is the key R package helping to test shiny web app functionality
Pandoc and knitr – The Pandoc software comes bundled with RStudio, and is a “swiss-army knife” for document conversion. There also is a function called [knitr::pandoc()] that is a wrapper that calls Pandoc to convert docs to HTML, PDF, etc.
PhantomJS maybe – This used to be needed for downloads (and was installed with
webshot::install_phantomjs()). But note this newer info:
NOTE FROM https://rstudio.github.io/shinytest2/articles/z-migration.html: - shinytest2 is the successor to shinytest. shinytest was implemented using webdriver which uses PhantomJS. PhantomJS has been unsupported since 2017 and does not support displaying bslib’s Bootstrap v5. shinytest2 uses chromote to connect to your locally installed Chrome or Chromium application, allowing shinytest2 to display bslib’s Bootstrap v5.
How shinytest2 Works
shinytest2 (here referred to henceforth as “shinytest” not to be confused with the older R package that was named shinytest!) automates shiny web app functionality testing, so we can determine if code updates break or unexpectedly modify parts of an application.
It runs the app in a headless Chrome or Chromium browser and simulates user interactions. The EJAM shinytest2 tests no longer compare full saved snapshots of app state, generated HTML, maps, tables, or downloaded files. Instead, they assert stable, intentional facts about app behavior. This avoids brittle failures from minor changes in generated HTML, dates, package versions, table rendering details, or map markup.
Key Features:
- Runs scripted app interactions for each supported web app test category
- Checks that the expected site-selection path is used for each category
- Checks uploaded filenames or FIPS picker selections where relevant
- Checks that analysis completion is exported by the app
- Checks selected inputs such as radius, title, plot controls, and picker values
- Checks downloaded reports and spreadsheets by file type, approximate size, and basic structure
- Checks that key tables, plots, and report outputs render in the
latlonfull-path test - Avoids checking exact full HTML reports, map markup, complete table contents, exact dates, or package version text
EJAM’s shinytest2 Folder Structure
R/test_ejam.R
tests/
└── testthat/
├── setup.R
├── setup-shinytest2.R
└── test-webapp-[DATA TYPE]-functionality.R (e.g. test-webapp-FIPS-functionality.R)
File Descriptions
testthat/setup.R– Does setup for the test environment (loadsglobal_*.Rand app scripts into the testing environment, etc.). This file is auto-sourced bytestthat.setup-shinytest2.R– Is also auto-sourced bytestthatbecause its filename starts withsetup-, and it contains the source code forshinytest2_webapp_functionality(), which does a series of tests of web app UI interactions using the app to upload or select, and analyze, multiple data types (FIPS, shapefile, latlon, NAICS, etc.).testthat/test-webapp-[DATA TYPE]-functionality.R– Each of these callsshinytest2_webapp_functionality(), specifying the data type to test with, after that helper has been made available by testthat’s automatic sourcing of the setup files.
Updating Tests
You may wish to modify the shinytest scripts, either to add new interactions with the application or to modify existing ones, such as in the case of an app component that can no longer be interacted with. Here are some methods and tips for updating the shinytest script accordingly.
Direct Updates
Modify source code of shinytest2_webapp_functionality()
to add new interactions with the app for the shinytest to test. You
would update that file by coding new UI interactions directly.
Using shinytest2::record_test() to generate testing
code
If you’re not sure how to code new UI interactions directly, run
shinytest2::record_test() to test the app interactively and
record your actions, which can then be copied into test scripts.
Using shiny::exportTestValues(name = value)
Throughout the app code, shiny::exportTestValues() can
be used to expose values from reactive expressions or other items
that are not inputs or outputs. The shinytest2 code can then read
those exported values with app$get_values(export = TRUE) or
wait for them with app$wait_for_value(export = "name").
EJAM uses this pattern for stable checks such as
analysis_complete and
multisite_report_download_ready. See details in
shinytest2_webapp_functionality() as defined in the
/tests/testthat/ folder.
Running Tests Locally
One way to run the shinytests is via the GitHub Actions.
Another way is this:
x = EJAM:::test_ejam(ask=F, run_these="webapp")You can also run a test directly in an interactive R session. This is useful for debugging one category at a time.
remotes::install_local() # once
library(EJAM) # once
source(testthat::test_path("setup.R")) # once. gets done automatically though, by things like testthat::test_file()
# run a single test:
shinytest2_webapp_functionality("latlon")It is recommended during development to use
remotes::install_local() to ensure your development code is
the one tested. This is because shinytest2 automatically references the
installed version of a package.
Another useful way was this (but this might be deprecated by shinytest2)
# first, source `setup.R`, from the tests/testthat/ folder.
source(testthat::test_path("setup.R"))
# then for one subset of tests, like just the latlon analysis features:
shinytest2::test_app(".", filter="latlon-functionality", check_setup = FALSE)
# for all the webapp functionality tests
shinytest2::test_app(".", filter="-functionality", check_setup = FALSE)GitHub Actions Integration
Using GitHub Actions (GHA) we can have GitHub run our shiny web app UI tests prior to merging a Pull Request, to give us peace of mind that the app will still work with the merged code.
Workflow
- The GHA sets up R, installs dependencies, and runs scripted shinytest2 tests.
- The workflow is stored in
.github/workflows/test-webapp-functionality.yaml. - PRs to a specified branch such as
development,maincan trigger GHA workflows (as specified in the workflow yml file).
Speed Optimization
- If GHA takes too long, cache dependencies by temporarily disabling steps after setup.
- Keep slow checks centralized. For example, the
latlonshinytest2 test runs the broad report download, spreadsheet download, details table, and plot checks; other categories should focus on category-specific selection and analysis checks.
Updating Expected Behavior Checks
Do not use testthat::snapshot_accept() for these
shinytest2 tests. If a test fails, inspect whether the app behavior
changed in a meaningful way. If the behavior is still correct, update
the explicit assertion in setup-shinytest2.R so it checks a
stable fact instead of exact generated output.
Examples of stable checks include:
- an uploaded file input contains the expected filename
- a FIPS picker contains the expected selected FIPS code
-
analysis_completeisTRUE - a downloaded report exists, has an
.htmlextension, is above a minimum size, and contains a few stable markers - a downloaded spreadsheet exists, has an
.xlsxextension, has the expected ZIP signature, and contains required sheet names - a plot output has rendered an image with nonzero dimensions
Debugging Tests & GitHub Actions
Debugging shinytest2
- Use
save_log()to inspect logs. - Add
print(),message(), orwarning()statements in shinytest2_webapp_functionality(). - Run, line-by line or in chunks, the main shinytest code in shinytest2_webapp_functionality()
Then, after running lines or chunks, run app$get_log()
to view the log.
Debugging GHA
Generally, if the shinytest2 tests pass locally using the same branch and dependencies, GHA should pass. However, the tests can still fail due to OS differences, browser availability, R version differences, package differences, or timeouts. Here are some tips for debugging these issues:
- Inspect the log in the GitHub repo, under the Actions tab or the Checks tab of the PR.
- Inspect artifacts or logs after a failed run to identify whether the failure was an app behavior regression, browser startup problem, timeout, missing dependency, or assertion that is too brittle.
Current State of Tests
- If the shinytests are failing, do not accept new snapshots. Inspect the failing assertion and decide whether app behavior regressed or the test should check a more stable fact.
- The versions of R and shiny and shinytest2 used for testing also should be noted as potentially affecting tests.