How do I test an observeEvent that updates an input? / How do I test a module using shinytest?

I have a shiny module that I want to test (see code below) that is part of an app that I have packaged as an R package.

The reactive value timeunit_rv holds a state in the app using the module. It can be set by several controls, and as I update one, I want the other controls to be updated too.

I would like to write two tests, one for each of the two observeEvents:

  1. Test that if I update input$tunit, then timeunit_rv is updated.
  2. Test that if I update timeunit_rv, then input$tunit is updated.

I would like to use shiny::testServer, if possible.

Questions:

  1. Am I correct that it is not possible to use shiny::testServerbecause that function does not take into account the ui, and hence running updateSelectInput(session, "tunit", selected = timeunit_rv())will not update input$tunit?
  2. If testServer does not work, how do I best test my module? Would shinytest be my best option? How would I best use shinytest to test the module in isolation from the rest of the app?
TimeUnitUI <- function(id) {
  ns <- NS(id)
  selectInput(ns("tunit"), "Unit", choices = c("s", "m", "h"), selected = "h")
}

timeUnitServer <- function(id, timeunit_rv) {
  
  moduleServer(id, function(input, output, session) {
    
    observeEvent(input$tunit, {
      timeunit_rv(input$tunit)
    })
    
    observeEvent(timeunit_rv(), {
      updateSelectInput(session, "tunit", selected = timeunit_rv())
    })

  })
}

OK, since no one has answered and I did some more research I will try to offer an answer for others myself. Please, other readers, give feedback.

  1. Yes, you are.
  2. Yes, shinytest would be the best way. In particular, I enjoy the way shinytest is used in this article, where the author uses it as part of test_that calls.

However, that article does not describe how to test it as a module and inside a package. Let's start with the latter.

Package

I guess for the purpose of this question the difference between being part of a package and not mostly relates to where you place files and how you execute them. If not part of a package, you just follow the article above and execute your test script using testthat::test_dir() or testthat::test_file(), as described here.

If part of a package, I generally place my tests in tests/testthat and execute them using devtools::test().

Module

To test a module using shinytest, I put it inside a minimal test app. The test app is only for testing purposes, so I put it inside tests/testthat. Then I instantiate my minimal test app in the test using the following code and start testing

app <- shinytest::ShinyDriver$new("minimal_test_app.R", loadTimeout = 1e+05)
expect_equal(app$getValue("whatever"),   "this_is_the_correct_value")

The next problem is that the app needs to find the code containing the module. If not inside a package, you can just use source and library inside minimal_test_app.R to get the module and the required external libraries. If inside a package, I place the command pkg_load::load_all() as the first row of minimal_test_app.R. I guess the setup is a bit similar to what is described under " Application objects created by functions" here.

Comparison to using testthat::test_server

I find that the tests run quite a bit slower when using shinytest than when using testthat::testServer, perhaps because we are using a headless browser. Therefore, I would use testthat::testServer when possible. However, in cases such as the one above, where events trigger server calculations that in turn update the UI, shinytest is the only option that I am aware of.

Test fixtures

Creating a test app such as this one is a type of test fixture, and should be cleaned up after the test.