mirror of
https://github.com/rstudio/shiny.git
synced 2026-02-09 22:25:59 -05:00
157 lines
5.0 KiB
Plaintext
157 lines
5.0 KiB
Plaintext
---
|
|
title: "Integration Testing in Shiny"
|
|
output: rmarkdown::html_vignette
|
|
vignette: >
|
|
%\VignetteEngine{knitr::rmarkdown}
|
|
%\VignetteIndexEntry{Your Vignette Title}
|
|
%\VignetteEncoding{UTF-8}
|
|
---
|
|
|
|
```{r setup, include=FALSE}
|
|
knitr::opts_chunk$set(echo = TRUE)
|
|
```
|
|
|
|
## Introduction to Inspecting Modules
|
|
|
|
First, we'll define a simple Shiny module:
|
|
|
|
```{r}
|
|
library(shiny)
|
|
module <- function(input, output, session) {
|
|
rv <- reactiveValues(x = 0)
|
|
observe({
|
|
rv$x <- input$x * 2
|
|
})
|
|
output$txt <- renderText({
|
|
paste0("Value: ", rv$x)
|
|
})
|
|
}
|
|
```
|
|
|
|
This module
|
|
|
|
- depends on one input (`x`),
|
|
- has an intermediate, internal `reactiveValues` (`rv`) which updates reactively,
|
|
- and updates an output (`txt`) reactively.
|
|
|
|
It would be nice to write tests that confirm that the module behaves the way we expect. We can do so using the `testModule` function.
|
|
|
|
```{r}
|
|
testModule(module, {
|
|
cat("Initially, input$x is NULL, right?", is.null(input$x), "\n")
|
|
|
|
# Give input$x a value.
|
|
input$x <- 1
|
|
|
|
cat("Now that x is set to 1, rv$x is: ", rv$x, "\n")
|
|
cat("\tand output$txt is: ", output$txt, "\n")
|
|
|
|
# Now update input$x to a new value
|
|
input$x <- 2
|
|
|
|
cat("After updating x to 2, rv$x is: ", rv$x, "\n")
|
|
cat("\tand output$txt is: ", output$txt, "\n")
|
|
})
|
|
```
|
|
|
|
There are a few things to notice in this example.
|
|
|
|
First, the test expression provided here assumes the existence of some variables -- specifically, `input`, `output`, and `r`. This is safe because the test code provided to `testModule` is run in the module's environment. This means that any parameters passed in to your module (such as `input`, `output`, and `session`) are readily available, as are any intermediate objects or reactives that you define in the module (such as `r`).
|
|
|
|
Second, you'll need to give values to any inputs that you want to be defined; by default, they're all `NULL`. You can either do this in-line as we've done above, or by specifying a value for the `initialState` parameter of the `testModule` function as we'll show below.
|
|
|
|
Last, you're likely used to assigning to `output`, but here we're reading from `output$txt` in order to check its value. When running inside `testModule`, you can simply reference an output and it will give the value produced by the `render` function.
|
|
|
|
## Automated Tests
|
|
|
|
Realistically, we don't want to just print the values for manual inspection; we'll want to leverage them in automated tests. That way, we'll be able to build up a collection of tests that we can run against our module in the future to confirm that it always behaves correctly. You can use whatever testing framework you'd like (or none a all!), but we'll use the `expect_*` functions from the testthat package in this example.
|
|
|
|
```{r}
|
|
# Bring in testthat just for its expectations
|
|
suppressWarnings(library(testthat))
|
|
|
|
# Specify an initial value of x=1 in the `initialState` parameter below
|
|
testModule(module, {
|
|
expect_equal(rv$x, 2)
|
|
expect_equal(output$txt, "Value: 2")
|
|
|
|
input$x <- 2
|
|
expect_equal(rv$x, 4)
|
|
expect_equal(output$txt, "Value: 4")
|
|
}, initialState = list(x=1))
|
|
```
|
|
|
|
If there's no error, then we know our tests ran successfully. If there were a bug, we'd see an error printed. For example:
|
|
|
|
```{r}
|
|
tryCatch({
|
|
testModule(module, {
|
|
expect_equal(rv$x, 99)
|
|
}, initialState = list(x=1))
|
|
}, error=function(e){
|
|
print("There was an error!")
|
|
print(e)
|
|
})
|
|
```
|
|
|
|
## Promises
|
|
|
|
`testModule` can handle promises inside of render functions.
|
|
|
|
```{r}
|
|
library(promises)
|
|
library(future)
|
|
plan(multisession)
|
|
|
|
module <- function(input, output, session){
|
|
output$async <- renderText({
|
|
# Stash the value since you can't do reactivity inside of a promise. See
|
|
# https://rstudio.github.io/promises/articles/shiny.html#shiny-specific-caveats-and-limitations
|
|
t <- input$times
|
|
|
|
# A promise chain that repeats the letter A and then collapses it into a string.
|
|
future({ rep("A", times=t) }) %...>%
|
|
paste(collapse="")
|
|
})
|
|
}
|
|
|
|
testModule(module, {
|
|
# initialState below sets times=3 by default.
|
|
expect_equal(output$async, "AAA")
|
|
|
|
input$times <- 5
|
|
expect_equal(output$async, "AAAAA")
|
|
}, initialState = list(times=3))
|
|
```
|
|
|
|
As you can see, no special precautions were required for a `render` function that uses promises. Behind-the-scenes, the code in `testModule` will block when trying to read from an `output` that returned a promise. This allows you to interact with the outputs in your tests as if they were synchronous.
|
|
|
|
TODO: What about internal reactives that are promise-based? We don't do anything special for them...
|
|
|
|
## Modules with additional inputs
|
|
|
|
TODO.
|
|
Probably something like...
|
|
|
|
```
|
|
module <- function(input, output, session, arg1, arg2){
|
|
# ...
|
|
}
|
|
|
|
testModule(module, {
|
|
# ...
|
|
}, arg1=val1, arg2=val2)
|
|
```
|
|
|
|
## Accessing a module's returned value
|
|
|
|
TODO: probably hang it off of `session$` somewhere.
|
|
|
|
## Complex Outputs (plots, htmlwidgets)
|
|
|
|
TODO
|
|
|
|
## Testing Servers
|
|
|
|
TODO: Will show that you can pass in a shinyapp object or a server function to a similar function called `testServer` and it will work similarly.
|