Comprehensive Guide to Testing Shiny Modules with shiny::testServer

Learn how to effectively test Shiny modules using shiny::testServer with examples and best practices.

rshinytests
3 min read

Modules tested with testServer run with a session that is a MockShinySession object.

For most cases we only need:

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:

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:

Tests help define what should be the input to the module:

Such modules are easier to reuse and easier to compose them to build the whole app.