packages
library(testthat)
library(ggplot2)
library(dplyr)
library(shiny)
library(vdiffr)
library(shinytest2)Part 2 (series): Using testthat with Shiny outside of a package
Martin Frigaard
May 7, 2023
This series on testing has been updated with recent changes in testthat, shinytest2, and other packages to improve testing.
This is the second post in a series on testing Shiny applications. I’ll cover testing Shiny module server functions using the testhat package outside of an R package structure. The noap branch of the sapkgs.
This post covers how shiny::testServer() works using a simple Shiny application. The code for abcApp() is an RStudio project (i.e., there is a noap.Rproj file in the parent folder), but it’s not part of an R package. Developing shiny applications as R packages is highly recommended, but it’s possible to begin writing unit tests before your application is a fully developed shiny app-package.
For more information regarding performing tests outside of the package environment, see this issue on GitHub.
testthat is designed to perform unit tests in R packages, but not all Shiny apps begin as R packages. The Shiny application we’ll be using for this demonstration has been written using Shiny modules and a single utility function.
The standalone application function (launchApp()) is stored in app.R, the modules are contained in modules.R, and the single utility function is stored in utils.R:
├── README.md
├── app.R
├── modules.R
├── sapkgs.Rproj
├── tests/
│ ├── testthat/
│ │ ├── test-mod_abc_server.R
│ │ └── test-num_super_script.R
│ └── testthat.R
└── utils.RThe tests/ folder contains the following:
tests/ has a testthat.R ‘test runner’ filetests/testthat/ (see example test-mod_abc_server.R below):In this small example app, both ui and server modules are stored in the modules.R file.
UI module:
The counterpart to mod_abc_ui() is mod_abc_server():
Server module:
# server module
mod_abc_server <- function(id) {
moduleServer(id, function(input, output, session) {
# reactive
letter <- reactive({
LETTERS[input$num]
})
# super script
sup_scrpt <- reactive({
num_super_script(x = input$num)
})
# output
output$txt <- renderUI({
HTML(
paste0(
em(
"The ", code(input$num), code(sup_scrpt()),
" letter in the alphabet is: ", code(letter())
)
)
)
})
output$out <- renderPrint({
HTML(
paste0(
em(
"The ", code(input$num), code(sup_scrpt()),
" letter in the alphabet is: ", code(letter())
)
)
)
})
})
}The mod_abc_server() function uses the num_super_script() function stored in utils.R:
Utility function:
# utility function
num_super_script <- function(x) {
num <- as.numeric(x)
if (num < 0) {
stop("not a valid number")
} else if (num > 26) {
stop("not a valid number")
} else if (num == 0) {
super_script <- ""
} else if (num == 1 | num == 21) {
super_script <- "st"
} else if (num == 2 | num == 22) {
super_script <- "nd"
} else if (num == 3 | num == 23) {
super_script <- "rd"
} else {
super_script <- "th"
}
return(super_script)
}Standalone app functions include a call to shiny::shinyApp():
The call to shiny::shinyApp() is placed inside the launchApp() function
The ui argument is wrapped in shiny::fluidPage() with the ui module function (mod_abc_ui()) placed inside fluidRow()
The server argument includes the standard function(input, output, session) and the module server companion function–mod_abc_server()–with a matching id arguments
Because launchApp() is not part of a package, shiny and testthat are loaded and the modules and utility function are sourced in the top of the app.R file.
testServer()In the test-mod_abc_server.R file, I’ll add testServer() and include the module server function as the first argument:
app is the module server function (mod_abc_server) or any shiny.appobj
input$sThe first test I’ll add will check the initial value of input$num
cat()testServer() allows me to set new input values with session$setInputs()
Use session$setInputs() to set input$num to 3
input$num has changed (we’ll also add another custom message with cat())The module’s reactive values are also available to in testServer().
Test 3 adds a test for sup_scrpt() (given the changed value of input$num)
expected value is what I’m expecting num_super_script() to return:letter()output$sThe module output values are also available as output$<value>.
The final test will verify this object is a list and print the results to the Console
output$txt has been updated with input$num:test_file():[ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
Test 1 initial input$num = NULL: TRUE
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ]
Test 2 setInputs(num = 3): 3
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 3 ]
Test 3 sup_scrpt(): = 'rd': rd
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 4 ]
Test 4 letter() = C: C
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 5 ]$html
<em>
The
<code>3</code>
<code>rd</code>
letter in the alphabet is:
<code>C</code>
</em>The results show the tests passed! Now I am confident inputs, reactive values (sup_scrpt() & letter()), outputs behave as expected.
The example above provides a workflow for using testServer() with testthat outside a package environment. The checklist below summarizes the steps required to test your application’s module server functions:
Create test files (in tests/testthat/)
Verify inputs with session$setInputs(inputId = <value>)
Test reactive values by referring to them as you would in the module server
Test outputs using output$<value> to check changes to the inputs and reactives
This concludes running tests on noap. Ideally, Shiny applications are developed as an R package (which I’ll cover in future posts), but now you know how to perform tests if this isn’t the case. The files for this demonstration are located here..
For a more comprehensive review of testing, check out the chapters on testing in R packages and Mastering Shiny.