13  Testing modules


Testing modules
  • Use session$returned() to check server function outputs, ensuring modules respond correctly to inputs.

  • Apply session$flushReact() to test reactive elements immediately, verifying reactive behavior within modules.

  • Parameterize with args = list() for flexible testing, simulating various user scenarios and inputs efficiently.

  • Aim for efficient module test coverage, identifying and testing critical functionality paths to guarantee module reliability.


In the previous chapters we added fixtures and helpers to our test suite and how to use them while running unit tests. In this chapter, we’re going briefly discuss some tips for testing modules with testServer()–specifically, how to verify modules are transferring values correctly.

I’ve created the shinypak R package In an effort to make each section accessible and easy to follow:

Install shinypak using pak (or remotes):

# install.packages('pak')
pak::pak('mjfrigaard/shinypak')

Review the chapters in each section:

library(shinypak)
list_apps(regex = 'modules')
## # A tibble: 1 × 2
##   branch           last_updated       
##   <chr>            <dttm>             
## 1 13_tests-modules 2024-07-02 13:43:56

Launch an app:

launch(app = "13_tests-modules")

13.1 Integration tests

Integration tests verify that functions and components work together, and often involves instantiating multiple objects to interact with each other in a single test.

Launch app with the shinypak package:

launch('13_tests-modules')

We can combine the BDD functions with testServer() to test reactive interactions between modules. For example, to confirm that the drop-down feature requirement is working (i.e., that user-inputs are updating in the application), we need to test two changes:

  1. Values passed to the UI are returned from mod_var_input_server()
  2. The reactive values returned from mod_var_input_server() are passed into mod_scatter_display_server() and available as the reactive object inputs()

In BDD, requirements are written plain language ‘feature files’ using a series of keywords:

Feature:
  As a
  I want
  So that
  
  Background:
    Given
    And
    
  Scenario:
    When
    And
    Then
1
High-level description (title and description)
2
Steps or conditions that exist before each scenario
3
Used to describe the initial context or preconditions for the scenario
4
A series of steps outlining a concrete examples that illustrates a feature
5
Used to describe an event, or an action
6
Use to combine Given, When, or Then
7
Use to verify expected outcomes that are observable by a user

Feature and Background information can be included in nested describe() blocks, but every Scenario (i.e., Then) keyword should have a corresponding it() or test_that() call.

Read more about Gherkin on the Cucumber website..

The feature, background, and scenario for the changes in mod_var_input_server() are provided below:

describe(
  "Feature: Scatter Plot Configuration in Movie Review Application
      As a user 
      I want the initial graph pre-configured with variables and aesthetics,
      So that I can change the inputs and see a meaningful visualization.", 
  code = {
  
  describe(
    "Background: Initial scatter plot x, y, color values 
         Given the movie review application is loaded
         And the scatter plot initial x-axis value is [IMDB Rating] 
         And the scatter plot initial y-axis value is [Audience Score]
         And the scatter plot initial color value is [MPAA Rating]
         And the initial opacity of the points is set to [0.5]
         And the initial size of the points is set to [2]
         And the initial plot title is set to [Enter plot title]", code = {
           
  it("Scenario: Changing scatter plot x, y, color values
       Given the movie review application is loaded
       When I choose the [Critics Score] variable for the x-axis
       And I choose the [Runtime] variable for the y-axis
       And I choose the [Title type] variable for color
       Then the scatter plot should show [Critics score] on the x-axis
       And the scatter plot should show [Runtime] on the y-axis
       And the points on the scatter plot should be colored by [Title type]
       And the opacity of the points should be set to [0.5]
       And the size of the points should be set to [2]
       And the plot title should be [Enter plot title]", code = {
         
         shiny::testServer(app = mod_var_input_server, expr = {
            
           # test code ----- 
           
         })  
       })
    })
})

13.1.1 session$returned()

Inside testServer(), we can create a list of initial graph inputs for mod_var_input_server(), then pass identical values to session$setInputs(), and confirm the returned object with session$returned():1

    shiny::testServer(app = mod_var_input_server, expr = {
      
      test_logger(start = "var_inputs", msg = "initial returned()")
      
      # create list of output vals
      test_vals <- list(y = "imdb_rating",
                        x = "audience_score",
                        z = "mpaa_rating",
                        alpha = 0.75,
                        size = 3,
                        plot_title = "Example title")

      # change inputs
      session$setInputs(y = "imdb_rating",
                        x = "audience_score",
                        z = "mpaa_rating",
                        alpha = 0.75,
                        size = 3,
                        plot_title = "Example title")

      testthat::expect_equal(
        object = session$returned(),
        expected = test_vals
      )

      test_logger(end = "var_inputs", msg = "initial returned()")
      
})
1
Call to testServer()
2
Create output values for comparison
3
Set each input using setInputs(input = )
4
Confirm returned values against test_vals

The test above confirms the initial values can be passed and returned from mod_var_input_server().

13.1.2 session$flushReact()

If we want to test changing inputs, we should call session$flushReact() to remove the values set by session$setInputs() 2

shiny::testServer(app = mod_var_input_server, expr = {
    # flush reactives
    session$flushReact()
    test_logger(start = "var_inputs", msg = "updated returned()")
    # set inputs
    session$setInputs(y = "critics_score",
                      x = "runtime",
                      z = "title_type",
                      alpha = 0.5,
                      size = 2,
                      plot_title = "Enter plot title")

    testthat::expect_equal(object = session$returned(),
      expected = list(y = "critics_score",
                      x = "runtime",
                      z = "title_type",
                      alpha = 0.5,
                      size = 2,
                      plot_title = "Enter plot title"))
    
    test_logger(end = "var_inputs", msg = "updated returned()")
})
1
Call to testServer()
2
Flush reactives from previous expect_equal()
3
Set changed input values using setInputs(input = )
4
Confirm returned values against session$returned()

The final result of running test_active_file() on test-mod_var_input.R is below:

[ FAIL 0 | WARN 0 | SKIP 0 | PASS 0 ]
INFO [2023-11-08 20:00:39] [ START var_inputs = initial returned()]
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 1 ]
INFO [2023-11-08 20:00:39] [ END var_inputs = initial returned()]

INFO [2023-11-08 20:00:39] [ START var_inputs = updated returned()]
[ FAIL 0 | WARN 0 | SKIP 0 | PASS 2 ]
INFO [2023-11-08 20:00:39] [ END var_inputs = updated returned()]

13.1.3 args = list()

Now that we’ve confirmed mod_var_input_server() is returning the initial updated values, we want to make sure reactive values are passed correctly into mod_scatter_display_server().

In movies_server(), when we pass selected_vars to the var_inputs argument, we’re not passing the returned values (this is why we don’t need the parentheses). We’re calling on the method (or function) created by the call to reactive() (inside mod_var_input_server()).

I’ve included the movies_server() function below to refresh our memory of how this should work:3

movies_server <- function(input, output, session) {

      selected_vars <- mod_var_input_server("vars")

      mod_scatter_display_server("plot", var_inputs = selected_vars)
      
}
1
Calls return(reactive(list(...)))

When we pause execution with Posit Workbench’s debugger we can see the difference between calling selected_vars and selected_vars():

Browse[1]> selected_vars
reactive({
    list(
      y = input$y, 
      x = input$x, 
      z = input$z, 
      alpha = input$alpha, 
      size = input$size, 
      plot_title = input$plot_title
      )
})

 

Browse[1]> selected_vars()
$y
[1] "audience_score"

$x
[1] "imdb_rating"

$z
[1] "mpaa_rating"

$alpha
[1] 0.5

$size
[1] 2

$plot_title
[1] ""

We’ll cover using browser() and the IDE’s debugger more the debugging chapter.

The feature and scenario for the functionality above is captured in testthat’s BDD functions below:

describe(
  "Feature: Scatter Plot Configuration in Movie Review Application
      As a user 
      I want the initial graph pre-configured with variables and aesthetics,
      So that I can immediately see a meaningful visualization.",
  code = {
    it(
      "Scenario: Scatter plot initial x, y, color values 
         Given the movie review application is loaded
         When I view the initial scatter plot
         Then the scatter plot should show 'IMDB Rating' on the x-axis
         And the scatter plot should show 'Audience Score' on the y-axis
         And the points on the scatter plot should be colored by 'MPAA Rating'
         And the size of the points should be set to '2'
         And the opacity of the points should be set to '0.5'
         And the plot title should be 'Enter plot title'",
      code = {
        
      })
  })

Inside testServer(), if we’re testing a module function that collects the reactive values, we need to wrap those values in reactive() in the args() argument: 4

shiny::testServer(
  app = mod_scatter_display_server,
  args = list(
    var_inputs =
      shiny::reactive(
        list( 
            x = "critics_score",
            y = "imdb_rating",
            z = "mpaa_rating",
            alpha = 0.5,
            size = 2,
            plot_title = "Enter Plot Title"
          )
        )
  ),
  expr = {
    test_logger(start = "display", msg = "selected_vars initial values")
    testthat::expect_equal(
      object = inputs(),
      expected = list(
        x = "critics_score",
        y = "imdb_rating",
        z = "mpaa_rating",
        alpha = 0.5,
        size = 2,
        plot_title = "Enter Plot Title"
      )
    )
    test_logger(end = "display", msg = "selected_vars initial values")
})
1
List of reactive variable inputs
2
Compare inputs() to initial values

I’ve included the example above because it’s not included on the testServer() documentation, and I’ve found this method works well if you want to confirm two modules are communicating (i.e., returning and collecting outputs). System test with shinytest2 are a better option if we’re trying to capture a more comprehensive execution path (i.e., user scenario) in the application.

13.2 Module test coverage

When we check the code coverage for the test above, we can see it confirms var_inputs is communicating the reactive values to inputs() in mod_scatter_display_server(), but this test doesn’t execute the call to plotOutput():


Ctrl/Cmd + Shift + R

devtools:::test_coverage_active_file()

13.2.1 Testing output$s

To confirm the plot is being created properly in mod_scatter_display_server(), we can’t use the ggplot2::is.ggplot() function because the plot is being rendered by renderPlot(). However, we can verify the structure of the output$scatterplot object using any of the following expectations:

testthat::expect_true(
  object = is.list(output$scatterplot))

testthat::expect_equal(
  object = names(output$scatterplot),
  expected = c("src", "width", "height", "alt", "coordmap"))

testthat::expect_equal(
  object = output$scatterplot[["alt"]],
  expected = "Plot object")

It’s also possible to build the graph inside the test using the same code from the module server function, then confirm it with ggplot2::is.ggplot():

    plot <- scatter_plot(movies,
      x_var = inputs()$x,
      y_var = inputs()$y,
      col_var = inputs()$z,
      alpha_var = inputs()$alpha,
      size_var = inputs()$size) +
    ggplot2::labs(
      title = inputs()$plot_title,
      x = stringr::str_replace_all(
              tools::toTitleCase(inputs()$x), "_", " "),
      y = stringr::str_replace_all(
              tools::toTitleCase(inputs()$y), "_", " ")) +
    ggplot2::theme_minimal() +
    ggplot2::theme(legend.position = "bottom")
    
    testthat::expect_true(ggplot2::is.ggplot(plot))
1
Build graph (same code from module function)
2
Confirm ggplot2 object is built

If we’re still skeptical this test is confirming the plot is being built correctly, we can pass plot to print() in the test and the plot will appear in the Plots pane.

Passing plot to print() will send the graph to the Plots pane

Recap

Recap: testing modules


Testing modules

This chapter delves into the intricacies of testing Shiny modules. Let’s briefly recap the key points covered:

  • session$returned(): allows us to capture and examine the values returned by server-side functions, which is essential for validating the behavior of modules in response to user inputs and server-side processing.

  • session$flushReact() is crucial for testing reactive expressions and observers.

    • Using session$flushReact() forces the reactive system to execute, enabling us to test the outcomes of reactive expressions and observe their effects within the context of the module’s functionality.
  • args = list(): We discussed the importance of parameterizing module server functions using args = list() to facilitate more flexible and comprehensive testing.

    • parameterizing modules can easily simulate various scenarios and inputs, enhancing test coverage and the robustness of each module’s functionality.
  • Module Test Coverage: we outlined strategies for identifying critical paths through our module’s functionality, testing a range of inputs and user interactions, and ensuring that tests are efficient and maintainable.

Please open an issue on GitHub


  1. Read more about returned values in the section titled, ‘Modules with return values’ in the Shiny documentation.↩︎

  2. Read more about flushing reactive values in the section titled, ‘Flushing Reactives’ in the Shiny documentation.↩︎

  3. selected_vars are the reactive plot values returned from mod_var_input_server().↩︎

  4. Read more about adding parameters to testServer() in the section titled, ‘Modules with additional parameters’ in the Shiny documentation.↩︎