Skip to content

Instantly share code, notes, and snippets.

@alandipert
Created April 9, 2020 21:14
Show Gist options
  • Save alandipert/c28a84e71e7d6cf6723f63bded9d5963 to your computer and use it in GitHub Desktop.
Save alandipert/c28a84e71e7d6cf6723f63bded9d5963 to your computer and use it in GitHub Desktop.

testServer QA Notes

testServer() is a new function for testing the behavior of reactives inside of Shiny server functions and modules.

Server and module functions

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 testServer() solution

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.

The expr environment

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.

Steps to QA Shiny app server function testing

  1. In the RStudio IDE, create a new Shiny app with File > New Project > New Directory > Shiny Web Application

  2. 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')
  })
}

  1. Save app.R and press the Run 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 move bins to the top-level of the server function, and made it a reactive.

  2. Create a file tests/test.R with the following contents:

library(shiny)

testServer("../", {
  stop("nope")
})
  1. 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....
  1. Run the tests again, but save the result to a variable: results <- runTests()

  2. 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.

  1. Around line 36 of app.R, modify the server function so its arguments read (input, output, session) instead of (input, output). Save app.R with your changes.

  2. Run tests again and save the result: results <- runTests()

  3. Run results$error[[1]] and observe the following output:

<simpleError in rlang::eval_tidy(quosure, makeMask(session$env), rlang::caller_env()): nope>
  1. Now, let's make the test pass. Return to tests/test.R and replace the line stop("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.

  1. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment