Comprehensive Guide to Testing Shiny Modules with shiny::testServer

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

3 min read

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.