Anatomy Of A Shiny Module Test With shiny::testServer

3 min read

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

For most cases we only need:

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
    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")
      )
    })
  })
})
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")
      )
    })
  })
})

We arrange parameters to pass to the module.

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")
      )
    })
  })
})

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
      session$setInputs(select = c("Sepal.Length", "Species"))

      # Assert
      expect_equal(
        colnames(session$returned()),
        c("Sepal.Length", "Species")
      )
    })
  })
})

Code within testServer has access to the session, inputs and outputs.

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")
      )
    })
  })
})

We select 2 variables using an input that has "select" ID.

It will be accessed with input$select.

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")
      )
    })
  })
})

The return value should be a column subset of the data.

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")
      )
    })
  })
})

We use session$returned() to get the value of the returned reactive.

To use shiny::testServer the module must be implemented with shiny::moduleServer.

server <- function(id) {
  moduleServer(id, function(input, output, session) {
    # ...
  })
}
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]))
  })
}

And this is the module that passes this test.

Writing tests first for Shiny modules helps to keep them:

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.