Testing Non-Package Shiny Apps

Part 2: Using testthat with Shiny outside of a package

shiny
testing
Author

Martin Frigaard

Published

May 7, 2023

packages
library(testthat)
library(ggplot2)
library(dplyr)
library(shiny)
library(vdiffr)
library(shinytest2)

This is the second post in a series on testing Shiny applications. I’ll cover testing Shiny module server functions using the testhat package outside of an R package structure. The noap branch of the sapkgs.

Testing module server functions

This post covers how shiny::testServer() works using a simple Shiny application. The code for abcApp() is an RStudio project (i.e., there is a noap.Rproj file in the parent folder), but it’s not part of an R package. Developing shiny applications as R packages is highly recommended, but it’s possible to begin writing unit tests before your application is a fully developed shiny app-package.

For more information regarding performing tests outside of the package environment, see this issue on GitHub.

ABC App

testthat is designed to perform unit tests in R packages, but not all Shiny apps begin as R packages. The Shiny application we’ll be using for this demonstration has been written using Shiny modules and a single utility function.

The standalone application function (launchApp()) is stored in app.R, the modules are contained in modules.R, and the single utility function is stored in utils.R:

├── README.md
├── app.R
├── modules.R
├── sapkgs.Rproj
├── tests/
│   ├── testthat/
│   │   ├── test-mod_abc_server.R
│   │   └── test-num_super_script.R
│   └── testthat.R
└── utils.R
  • The tests/ folder contains the following:

    tests
    ├── testthat
    │   ├── test-mod_abc_server.R
    │   └── test-num_super_script.R
    └── testthat.R
    • tests/ has a testthat.R ‘test runner’ file
    • New test files should be placed in tests/testthat/ (see example test-mod_abc_server.R below):

UI module function

In this small example app, both ui and server modules are stored in the modules.R file.

  • UI module:

    mod_abc_ui() (example ui module function)
    # ui module
    mod_abc_ui <- function(id) {
      ns <- NS(id)
      tagList(
        column(
          width = 3,
          offset = 2,
          numericInput(
            inputId = ns("num"),
            label = "Alphabet Number",
            value = 5,
            min = 1,
            max = 26
          )
        ),
        column(
          width = 6,
          br(),
          uiOutput(
            outputId = ns("txt")
          ),
          verbatimTextOutput(ns("out"))
        )
      )
    }

Server module function

The counterpart to mod_abc_ui() is mod_abc_server():

  • Server module:

    mod_abc_server() (example server module function)
    # server module
    mod_abc_server <- function(id) {
      moduleServer(id, function(input, output, session) {
        # reactive
        letter <- reactive({
          LETTERS[input$num]
        })
        # super script
        sup_scrpt <- reactive({
          num_super_script(x = input$num)
        })
        # output
        output$txt <- renderUI({
          HTML(
            paste0(
              em(
                "The ", code(input$num), code(sup_scrpt()),
                " letter in the alphabet is: ", code(letter())
              )
            )
          )
        })
        output$out <- renderPrint({
          HTML(
            paste0(
              em(
                "The ", code(input$num), code(sup_scrpt()),
                " letter in the alphabet is: ", code(letter())
              )
            )
          )
        })
      })
    }

Module utility function

The mod_abc_server() function uses the num_super_script() function stored in utils.R:

  • Utility function:

    num_super_script() (example utility function)
    # utility function
    num_super_script <- function(x) {
          num <- as.numeric(x)
          if (num < 0) {
            stop("not a valid number")
          } else if (num > 26) {
            stop("not a valid number")
          } else if (num == 0) {
            super_script <- ""
          } else if (num == 1 | num == 21) {
            super_script <- "st"
          } else if (num == 2 | num == 22) {
            super_script <- "nd"
          } else if (num == 3 | num == 23) {
            super_script <- "rd"
          } else {
            super_script <- "th"
          }
        return(super_script)
    }

Standalone app function

  • Standalone app functions include a call to shiny::shinyApp():

    launch() (example app with modules)
    launchApp <- function() {
      shinyApp(
        ui = fluidPage(
          h2("ABC App"),
          fluidRow(
            mod_abc_ui("x")
          )
        ),
        server = function(input, output, session) {
          mod_abc_server("x")
        }
      )
    }
    launchApp()
    • The call to shiny::shinyApp() is placed inside the launchApp() function

    • The ui argument is wrapped in shiny::fluidPage() with the ui module function (mod_abc_ui()) placed inside fluidRow()

    • The server argument includes the standard function(input, output, session) and the module server companion function–mod_abc_server()–with a matching id arguments

Because launchApp() is not part of a package, shiny and testthat are loaded and the modules and utility function are sourced in the top of the app.R file.

source utils.R and modules.R in app.R
# packages --------------------------------------------------------
library(shiny)
library(testthat)

# utils ------------------------------------------------------------------
source("utils.R")

# modules ------------------------------------------------------------------
source("modules.R")

Using testServer()

In the test-mod_abc_server.R file, I’ll add testServer() and include the module server function as the first argument:

  • app is the module server function (mod_abc_server) or any shiny.appobj

    app = mod_abc_server
    testServer(app = mod_abc_server, {
    
    })

Testing input$s

  • The first test I’ll add will check the initial value of input$num

    • I’ll also include a custom message with cat()
    test initial value with custom message
    testServer(mod_abc_server, {
      # Test initial value
      testthat::expect_equal(input$num, NULL)
      cat("\n Test 1 initial input$num = NULL: ", is.null(input$num), "\n")
    })

testServer() allows me to set new input values with session$setInputs()

  • Use session$setInputs() to set input$num to 3

    • Test 2 confirms input$num has changed (we’ll also add another custom message with cat())
    setInputs() and test inputs
    testServer(mod_abc_server, {
      # set inputs
      session$setInputs(num = 3)
      # Test set inputs
      testthat::expect_equal(input$num, 3)
      cat("\n Test 2 setInputs(num = 3):", input$num, "\n")
    })

Testing reactive values

The module’s reactive values are also available to in testServer().

  • Test 3 adds a test for sup_scrpt() (given the changed value of input$num)

    • The expected value is what I’m expecting num_super_script() to return:
    Check sup_scrpt() reactive value with expect_equal()
    testServer(mod_abc_server, {
      # Test super script
      testthat::expect_equal(object = sup_scrpt(), expected = "rd")
      cat("\n Test 3 sup_scrpt(): = 'rd':", sup_scrpt(), "\n")
    })
    • For completeness we’ll add a test for letter()
    Check letter() reactive value with expect_equal()
    testServer(mod_abc_server, {
      # Test letter
      expect_equal(object = letter(), expected = "C")
      cat("\n Test 4 letter() = C:", letter(), "\n")
    })

Testing output$s

The module output values are also available as output$<value>.

  • The final test will verify this object is a list and print the results to the Console

    • Output tests can verify that output$txt has been updated with input$num:
    Check module output values
    testServer(mod_abc_server, {
      # Test output
      expect_true(is.list(output$txt))
      print(output$txt)
    })
    • Finally, I’ll run the tests with test_file():
    test_file(path = "/path/to/app/tests/testthat/")
    [ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
     Test 1 initial input$num = NULL:  TRUE 
    [ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ]
     Test 2 setInputs(num = 3): 3 
    [ FAIL 0 | WARN 0 | SKIP 0 | PASS 3 ]
     Test 3 sup_scrpt(): = 'rd': rd 
    [ FAIL 0 | WARN 0 | SKIP 0 | PASS 4 ]
     Test 4 letter() = C: C 
    [ FAIL 0 | WARN 0 | SKIP 0 | PASS 5 ]$html
    <em>
      The 
      <code>3</code>
      <code>rd</code>
       letter in the alphabet is: 
      <code>C</code>
    </em>

The results show the tests passed! Now I am confident inputs, reactive values (sup_scrpt() & letter()), outputs behave as expected.

Recap

The example above provides a workflow for using testServer() with testthat outside a package environment. The checklist below summarizes the steps required to test your application’s module server functions:

  1. Create test files (in tests/testthat/)

  2. Verify inputs with session$setInputs(inputId = <value>)

  3. Test reactive values by referring to them as you would in the module server

  4. Test outputs using output$<value> to check changes to the inputs and reactives

This concludes running tests on noap. Ideally, Shiny applications are developed as an R package (which I’ll cover in future posts), but now you know how to perform tests if this isn’t the case. The files for this demonstration are located here..

For a more comprehensive review of testing, check out the chapters on testing in R packages and Mastering Shiny.