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: public
Modify 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