Comprehensive Guide to Testing Shiny Modules with shiny::testServer
Learn how to effectively test Shiny modules using shiny::testServer with examples and best practices.
Modules tested with testServer
run with a session that is a MockShinySession
object.
For most cases we only need:
MockShinySession$setInputs()
method which simulates users interactions with widgets.MockShinySession$returned
which contains the value returned by the module.
Let’s take a look an example.
We develop a module that takes a dataset as a parameter, and returns a subset of the data based on the selected variables in a dropdown.
describe("server", {
it("should subset the data with selected variables", {
# Arrange
# Act
# Assert
})
})
We arrange parameters to pass to the module.
describe("server", {
it("should subset the data with selected variables", {
# Arrange
args <- list(data = iris)
# Act
# Assert
})
})
Arguments are passed to the module as a list.
describe("server", {
it("should subset the data with selected variables", {
# Arrange
args <- list(data = iris)
shiny::testServer(server, args = args, {
# Act
# Act
# Assert
# Assert
})
})
})
Code within testServer
has access to the session, inputs and outputs. We select 2 variables using an input that has "select"
ID. We assume that we will implement an input with this ID.
describe("server", {
it("should subset the data with selected variables", {
# Arrange
args <- list(data = iris)
shiny::testServer(server, args = args, {
# Act
session$setInputs(select = c("Sepal.Length", "Species"))
# Assert
})
})
})
The return value should be a column subset of the data
. We use session$returned()
to get the value of the returned reactive
.
describe("server", {
it("should subset the data with selected variables", {
# Arrange
args <- list(data = iris)
shiny::testServer(server, args = args, {
# Act
session$setInputs(select = c("Sepal.Length", "Species"))
# Assert
expect_equal(
colnames(session$returned()),
c("Sepal.Length", "Species")
)
})
})
})
To use shiny::testServer
the module must be implemented with shiny::moduleServer
.
server <- function(id) {
moduleServer(id, function(input, output, session) {
# ...
})
}
And this is a module that passes this test.
ui <- function(id) {
ns <- NS(id)
fluidPage(
selectInput(ns("select"), "Select variables", choices = NULL, multiple = TRUE),
)
}
server <- function(id, data) {
moduleServer(id, function(input, output, session) {
updateSelectInput(session, "select", choices = colnames(data))
return(reactive(data[, input$select]))
})
}
Notice how we don’t check the UI in this test. Using this test we don’t know if the module updated the input correctly.
Use shiny::testServer
to test low level behaviors of the module.
Use it to test contracts:
- if it returns the correct value,
- or if it runs a side effect.
This might be especially useful if this is a “low-level” module that is used in many places in the app.
Use {shinytest2}
to test both parts of a Shiny module.
Using {shinytest2}
can be better to test user behaviors by simulating real interactions with the app.
Writing tests first for Shiny modules helps to keep them:
- simple,
- focused,
- and doing exactly what they need to do.
Tests help define what should be the input to the module:
- what it should do,
- and what it should return.
Such modules are easier to reuse and easier to compose them to build the whole app.