testServer()
is a new function for testing the behavior of reactives inside of Shiny server functions and modules.
Server functions define the reactive behavior of Shiny applications. Together with UI functions, server functions define a Shiny application. Server functions are R functions such as the following:
server <- function(input, output, session) {
doubled <- reactive({ input$x * 2 })
output$txt <- renderText({ doubled() })
}
Module functions are R functions such as the following:
module <- function(id) {
moduleServer(id, function(input, output, session) {
doubled <- reactive({ input$x * 2 })
output$txt <- renderText({ doubled() })
})
}
In the above functions, the input input$x
is referenced inside of a "reactive expression", or fragment of R code passed to reactive()
. The value of the reactive()
call is assigned to the local variable doubled
.
Next, a relationship is established between changes to input$x
and output$txt
via doubled
using renderText()
. renderText()
, like reactive()
, takes a reactive expression as its argument. However, unlike reactive()
, renderText()
returns a value that may be assigned to an output such as output$txt
.
A data dependency graph has been formed. It looks like this:
input$x --> doubled --> output$txt
Data dependency graphs are formed in any program involving variables that depend on one another. The difference between other kinds of programs and functions like the one above is that values propagate through the graph after the server function or module has returned.
That is, a user may provide new values for input$x
over time. New user-provided values will propagate through the data dependency graph even after server()
and module()
are called and has returned.
This illustrates the primary difference between "normal" R functions and module and server functions. "Normal" R functions may contain variables, but the relationship between the variables last only until the function returns. In contrast, server and module functions define a "reactive" data dependency graft that endures after the function has returned.
Normal functions are easily tested, since their behavior over time is governmed by their arguments enclosing environment. To write assertions about their expected behavior, one needs only to call the function with particular arguments, and assert that the value returned is sensible.
Module and server functions are less easily tested because their behavior is determined by the behavior of the reactive data dependency graph they establish. They also usually don't return values that aid in testing.
The difficulty in testing module and server function behavior is the problem that the new testServer()
function addresses.
The new shiny::testServer()
function provides a way to retain and interact with the reactive graph produced by a server or module function. Using testServer()
, users can write code that sends new data into the graph, simulating the user action of setting an input. Users may also query the values of elements of the reactive graph at different points in time. By combining the ability to introduce values and query the effect of doing so, users have the ability to write unit tests involving reactive graphs, given a server or module function.
The syntax of testServer()
is:
testServer(<module> | <app>, <expr>, ...<args>)
where
<module> = A function that contains a call to `moduleServer()`.
<app> = An object, such a character vector indicating the path to a Shiny application, that may be coerced to a Shiny app obj by `shiny::as.shiny.appobj()`.
<expr> = An R code block or expression. The contents of the expression are evaluated in an environment in which the reactive graph created by the <module> or <app> is available for interaction. See below for details.
<args> = Zero or more arguments to supply to <module>. These arguments are discarded if the first argument to `testServer()` is an <app>. `id` is optional; if an `id` argument is not supplied, one will be generated.
Roughly speaking, the expr
argument to testServer()
is a block of code that gets evaluated as if it had been included in the bottom of the module or server function body.
Consider the following example:
module <- function(id) {
moduleServer(id, function(input, output, session) {
doubled <- reactive({ input$x * 2 })
output$txt <- renderText({ doubled() })
})
}
testServer(module, {
session$setInputs(x = 2)
stopifnot(doubled() == 4)
})
The above example demonstrates the availability of session
. input
and output
are also available, and correspond to the input
, output
, and session
names introduced by the module definition.
While not used, id
and any other names introduced by the outermost module function are also accessible within expr
.
The example also demonstrates the usage of session$setInputs()
. In normal operation, the session
object passed to module and server functions does not include a $setInputs()
method. This method is only available in expr
code passed to testServer()
. As the example demonstrates, $setInputs()
is used to set the values of inputs associated with the inputs
module function argument.
Finally, the module demonstrates the accessibility of the doubled()
reactive. Its value is accessed within a stopifnot()
call and compared with 4
.
-
In the RStudio IDE, create a new Shiny app with
File > New Project > New Directory > Shiny Web Application
-
Replace lines 36-46 of the resulting app.R with the following:
server <- function(input, output) {
x <- faithful[, 2]
bins <- reactive(seq(min(x), max(x), length.out = input$bins + 1))
output$distPlot <- renderPlot({
# generate bins based on input$bins from ui.R
x <- faithful[, 2]
# draw the histogram with the specified number of bins
hist(x, breaks = bins(), col = 'darkgray', border = 'white')
})
}
-
Save
app.R
and press theRun App
button at the top right of the editor pane. You should see the app run without error. The difference between the default app and our change is that we movebins
to the top-level of the server function, and made it a reactive. -
Create a file
tests/test.R
with the following contents:
library(shiny)
testServer("../", {
stop("nope")
})
- We have created a test file that exercises
testServer()
. You can run the app's test with the following command in the R console:runTests()
The tests will fail, and you should see the following output:
file pass result error
1 test.R FALSE NA Error in....
-
Run the tests again, but save the result to a variable:
results <- runTests()
-
Run
runTests()$error[[1]]
and observe the following output:
<simpleError in testServer("../", { stop("nope")}): Tested application server functions must declare input, output, and session arguments.>
This output indicates that we must change the definition of server
in our app.R
. In particular, we must add a session
argument to it in order for testServer()
to work.
-
Around line 36 of
app.R
, modify the server function so its arguments read(input, output, session)
instead of(input, output)
. Saveapp.R
with your changes. -
Run tests again and save the result:
results <- runTests()
-
Run
results$error[[1]]
and observe the following output:
<simpleError in rlang::eval_tidy(quosure, makeMask(session$env), rlang::caller_env()): nope>
- Now, let's make the test pass. Return to
tests/test.R
and replace the linestop("nope")
with the following, then save the file:
session$setInputs(bins = 10)
stopifnot(length(bins()) == 11)
This new test asserts that the length of the bins
reactive created in the server function is equal to 11 after input$bins
is set to 10.
- In the console, run
runTests()
again. Observe the following output:
file pass result error
1 test.R TRUE NA
Note that the result
column contains TRUE
instead of FALSE
. This indicates that our test has passed.