3

I've been exploring (**and loving) the golem package for developing modular dashboards with R Shiny. But I'm struggling to wrap my head around how to test a modular dashboard.

For example in the repex below how would I test that if the input$n_rows in the import module is set to 15 that the output in the display module contains 15 rows?

I'd be incredibly grateful for any support on this!


library(shiny)
library(reactable)
library(dplyr)

# Import module UI
mod_import_ui <- function(id){
  
  ns <- NS(id)
  
  fluidRow(
    # Allow the user to select the number of rows to view
    numericInput(ns("n_rows"), 
                 "Select number of observations",
                 value = 10)
    
  )
}

# Import module Server
mod_import_server <- function(id){
  
  moduleServer(
    id,
    function(input, output, session){
      
      data <- reactive({
        
        # Sample the requested number of rows from mtcars and return this to the application server
        mtcars %>%
          slice_sample(n = input$n_rows)
        # [....] # Some complex formatting and transformations
        
      })
      
      return(data)
      
      
      
    }
  )}

# Display module UI
mod_display_ui <- function(id){
  
  ns <- NS(id)
  
  fluidRow(
    
    reactableOutput(ns("table"))
    
  )
}

# Display module Server
mod_display_server <- function(id, data_in){
  
  moduleServer(
    id,
    function(input, output, session){
      
      # [....] # Some more transformations and merging with data from other modules
      
      output$table <- renderReactable(reactable(data_in()))
      
    }
  )}


app_ui <- function(request) { 
  
  tagList(
  
    mod_import_ui("import_1"),
    mod_display_ui("display_1")

  )
  
  }


app_server <- function(input, output, session) { 
  
  data_in <- mod_import_server("import_1")
  mod_display_server("display_1", data_in)
  
}

shinyApp(ui = app_ui, server = app_server)


David
  • 99
  • 4

2 Answers2

5

I would recommend separating the core of the app from the user interface.

The {golem} framework allows building your application inside a R package, which means you can use all tools from package build to document and test your code.
If you follow our guide in engineering-shiny.org/, you will see that we recommend extracting all R code from your "server" part to test it in a vignette, transform it as a regular function, so that you can test it as usual with R packages.
Hence, you ShinyApp only calls internal functions, already documented and tested. With this approach, you can test outputs of different scenarios that can happen in your application. Try different input parameters in a static script and verify outputs, whatever you change in your app in the next steps of your development.

The book gives a lot of advices. If I had to sum them up for a workflow, this would be:

  1. Build the necessary code in an Rmd directly. This allows you to test the operation without having to go through all the necessary clicks. We call it the "Rmd first" method: https://rtask.thinkr.fr/when-development-starts-with-documentation/
  2. Factorize this code into R functions to put as little as possible in the Shiny app itself.
  3. Create your UI part without server (or not too much), just to see what the general appearance looks like
  4. Include your functions in the appropriate places in the app.
  5. Strengthen the code. Reproducible examples, unit tests, documentation, code versioning, ... (This step is better when done in parallel with the code)
Sébastien Rochette
  • 6,536
  • 2
  • 22
  • 43
  • Thank you for this! I was a little uncertain how to include reactive or reactive values in a function (to be run outside of an R Shiny session). But I suppose you can just wrap your input in a function (for reactives) or build a list (for reactivevals). Thanks again! # With reactive `trim_data <- function(data, num) { data() %>% slice_sample(n = num) } get_mt <- function(){mtcars} trim_data(get_mt, 12)` `# With reactivevals trim_data2 <- function(r, num) { r$data %>% slice_sample(n = num) } r <- list(data = mtcars) trim_data2(r, 12)` – David Feb 10 '21 at 18:42
4

As a complement to Sebastien's answer, I would like to point that starting with {shiny} v 1.5.0, you can test server functions directly using the testServer function, which might be what you are looking for.

Here is a reprex of how you can achieve that:

library(shiny)
library(magrittr)
library(dplyr)

mod_import_server <- function(id){
  moduleServer( id, function(input, output, session){
    data <- reactive({
      mtcars %>%
        slice(n = 1:input$n_rows)
    })
    return(data)
  })
}

shiny::testServer(mod_import_server, {
  
  for (i in 1:10){
    
    session$setInputs(n_rows = i)
    testthat::expect_equal(
      data(), 
      slice(mtcars, n = 1:i)
    )
    
  }
})

Here, you can test that your reactive() behave the way you're expecting. That's not perfect but a good start :) It's hard to find a way to test for the behaviour of the second module though, as it depends on a reactive() passed as a value.

Colin

Dharman
  • 30,962
  • 25
  • 85
  • 135
Colin FAY
  • 4,849
  • 1
  • 12
  • 29