2022-11-14
David Granjon, Novartis
Senior Software developer at Novartis.
We’re in for 2 hours of fun!
Clone this repository with the RStudio IDE or via the command line.
Then run renv::restore() to install the dependencies.
If you want to run {shinyValidator} locally (not on CI/CD), you must have:
shinycannon installed for the load-test part. See here.
A chrome browser installed like chromium.
git installed and a GitHub account.
A recent R version, if possible R>= 4.1.0.
Your app may be as beautiful and as cool as you want, it is useless if it does not start/run.
How do we transition❓
Reliable : is the app doing what it is intended to do?
Stable : how often does it crash?
Available : is the app fast enough to handle multiple concurrent users?
In practice, a few apps meet all these requirements 😈.
Are there bottlenecks?
Not easy 😢
Can’t we make things easier❓
We create an empty golem project1:
We add some useful files, basic test and link to git:
Browse to GitHub and create an empty repository called <PKG> matching the previously created package.

Go to terminal tab under RStudio:
Initialize renv for R package dependencies:
system("echo 'RENV_PATHS_LIBRARY_ROOT = ~/.renv/library' >> .Renviron")
# SCAN the project and look for dependencies
renv::init()
# install missing packages
renv::install("<PACKAGE>")
# Capture new dependencies after package installation
renv::snapshot()system("echo 'RENV_PATHS_LIBRARY_ROOT = ~/.renv/library' >> .Renviron")
# SCAN the project and look for dependencies
renv::init()
# install missing packages
renv::install("<PACKAGE>")
# Capture new dependencies after package installation
renv::snapshot()system("echo 'RENV_PATHS_LIBRARY_ROOT = ~/.renv/library' >> .Renviron")
# SCAN the project and look for dependencies
renv::init()
# install missing packages
renv::install("<PACKAGE>")
# Capture new dependencies after package installation
renv::snapshot()system("echo 'RENV_PATHS_LIBRARY_ROOT = ~/.renv/library' >> .Renviron")
# SCAN the project and look for dependencies
renv::init()
# install missing packages
renv::install("<PACKAGE>")
# Capture new dependencies after package installation
renv::snapshot()
Review the file structure
audit_app() is the main function 1:
run_app() such as database logins, …This code is run during crash test, profiling and reactivity check.
Run the following code step by step1:
# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()# Start the app library(shinytest2) headless_app <- AppDriver$new("./app.R") # View the app for debugging (does not work from Workbench!) headless_app$view() headless_app$set_inputs(obs = 1) headless_app$get_value(input = "obs") # You can also run JS code! headless_app$run_js( "$('#obs') .data('shiny-input-binding') .setValue( $('#obs'), 100 ); " ) # Now you can call any function # Close the connection before leaving headless_app$stop()

run_crash_test() runs a gremlins.js test if no headless action are passed:
This is a modal window.
Beginning of dialog window. Escape will cancel and close the window.
End of dialog window.
./app.R in an external browser.
shinyValidator::audit_app(scope = "POC").public/index.html (external browser).Cleanup between each run!
Your turn 🎮
Modify shinyValidator::audit_app parameters:
usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()usethis::use_test("app-server-test")
# Inside app-server-test
testServer(app_server, {
session$setInputs(obs = 0)
# There should be an error
expect_error(output$distPlot)
session$setInputs(obs = 100)
str(output$distPlot)
})
# Test it
devtools::test()Run shinyValidator::audit_app and have a look at the coverage tab.
Leverage shinytest2 power1, app being the Shiny app to audit.
Run the above code and have a look at the screenshots.
Create this function in helpers.R:
Add it to app_server.R:
Enable output check in shinyValidator::audit_app:
Create a new test:
usethis::use_test("test-base-plot")
renv::install("vdiffr")
# Inside test-base-plot
test_that("Base plot OK", {
set.seed(42) # to avoid the test from failing due to randomness :)
vdiffr::expect_doppelganger("Base graphics histogram", make_hist(500))
})
# Test it
devtools::test()usethis::use_test("test-base-plot")
renv::install("vdiffr")
# Inside test-base-plot
test_that("Base plot OK", {
set.seed(42) # to avoid the test from failing due to randomness :)
vdiffr::expect_doppelganger("Base graphics histogram", make_hist(500))
})
# Test it
devtools::test()usethis::use_test("test-base-plot")
renv::install("vdiffr")
# Inside test-base-plot
test_that("Base plot OK", {
set.seed(42) # to avoid the test from failing due to randomness :)
vdiffr::expect_doppelganger("Base graphics histogram", make_hist(500))
})
# Test it
devtools::test()usethis::use_test("test-base-plot")
renv::install("vdiffr")
# Inside test-base-plot
test_that("Base plot OK", {
set.seed(42) # to avoid the test from failing due to randomness :)
vdiffr::expect_doppelganger("Base graphics histogram", make_hist(500))
})
# Test it
devtools::test()usethis::use_test("test-base-plot")
renv::install("vdiffr")
# Inside test-base-plot
test_that("Base plot OK", {
set.seed(42) # to avoid the test from failing due to randomness :)
vdiffr::expect_doppelganger("Base graphics histogram", make_hist(500))
})
# Test it
devtools::test()Modify the custom headless script by adding a timeout:
Run shinyValidator::audit_app and have a look at the profiling tab.

In case you need to control branches triggering {shinyValidator}:
If you have to change the R version, os, …:
- name: Lint code
shell: Rscript {0}
run: shinyValidator::lint_code()
- name: Audit app 🏥
shell: Rscript {0}
run: shinyValidator::audit_app()
- name: Deploy to GitHub pages 🚀
if: github.event_name != 'pull_request'
uses: JamesIves/github-pages-deploy-action@4.1.4
with:
clean: false
branch: gh-pages
folder: publicModify GitHub actions yaml file:
Get a top notch app? Try to setup {shinyValidator} and run it.
CI/CD and testing are not easy!


Follow me on Fosstodon. @davidgranjon@fosstodon.org
