packages
library(testthat)
library(ggplot2)
library(dplyr)
library(shiny)
library(vdiffr)
library(shinytest2)Part 2: Using testthat with Shiny outside of a package
Martin Frigaard
May 7, 2023
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.