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.R
The 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.