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
%%{init: {'theme':'dark'}}%%
flowchart TD
subgraph CICD
direction TB
subgraph DMC
direction LR
E[Lint] --> F[Quality]
F --> G[Performance]
end
subgraph POC
direction LR
H[Lint] --> I[Quality]
end
end
A(Shiny Project) --> B(DMC App)
A --> C(Poof of concept App POC)
B --> |strict| D[Expectations]
C --> |low| D
D --> CICD
CICD --> |create| J(Global HTML report)
J --> |deploy| K(Deployment server)
click A callback "Tooltip for a callback"
click B callback "DMC: data monitoring committee"
click D callback "Apps have different expectations"
click E callback "Lint code: check code formatting, style, ..."
click F callback "Run R CMD check + headless crash test (shinytest2)"
click G callback "Optional tests: profiling, load test, ..."
click J callback "HTML reports with multiple tabs"
click K callback "RStudio Connect, GitLab/GitHub pages, ..."
audit_app() is the main function 1:
run_app() such as database logins, …
%%{init: {'theme':'dark'}}%%
graph TD
A(Check) --> B(Crashtest)
B --> C(Loadtest)
C --> D(Coverage)
D --> E(Reactivity)
click A callback "devtools::check"
click B callback "{shinytest2}"
click C callback "{shinyloadtest}"
click D callback "{covr}"
click E callback "{reactlog}"
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
