context("testModule")
library(promises)
library(future)
plan(multisession)
test_that("testModule passes dots", {
module <- function(input, output, session, someArg) {
expect_false(missing(someArg))
expect_equal(someArg, 123)
}
testModule(module, {}, someArg = 123)
})
test_that("testModule passes dynamic dots", {
module <- function(input, output, session, someArg) {
expect_false(missing(someArg))
expect_equal(someArg, 123)
}
# Test with !!! to splice in a whole named list constructed with base::list()
moreArgs <- list(someArg = 123)
testModule(module, {}, !!!moreArgs)
# Test with !!/:= to splice in an argument name
argName <- "someArg"
testModule(module, {}, !!argName := 123)
})
test_that("testModule handles observers", {
module <- function(input, output, session) {
rv <- reactiveValues(x = 0, y = 0)
observe({
rv$x <- input$x * 2
})
observe({
rv$y <- rv$x
})
output$txt <- renderText({
paste0("Value: ", rv$x)
})
}
testModule(module, {
session$setInputs(x=1)
expect_equal(rv$y, 2)
expect_equal(rv$x, 2)
expect_equal(output$txt, "Value: 2")
session$setInputs(x=2)
expect_equal(rv$x, 4)
expect_equal(rv$y, 4)
expect_equal(output$txt, "Value: 4")
})
})
test_that("inputs aren't directly assignable", {
module <- function(input, output, session) {
}
testModule(module, {
session$setInputs(x = 0)
expect_error({ input$x <- 1 }, "Attempted to assign value to a read-only")
expect_error({ input$y <- 1 }, "Attempted to assign value to a read-only")
})
})
test_that("testModule handles more complex expressions", {
module <- function(input, output, session){
output$txt <- renderText({
input$x
})
}
testModule(module, {
for (i in 1:5){
session$setInputs(x=i)
expect_equal(output$txt, as.character(i))
}
expect_equal(output$txt, "5")
if(TRUE){
session$setInputs(x="abc")
expect_equal(output$txt, "abc")
}
})
})
test_that("testModule handles reactiveVal", {
module <- function(input, output, session) {
x <- reactiveVal(0)
observe({
x(input$y + input$z)
})
}
testModule(module, {
session$setInputs(y=1, z=2)
expect_equal(x(), 3)
session$setInputs(z=3)
expect_equal(x(), 4)
session$setInputs(y=5)
expect_equal(x(), 8)
})
})
test_that("testModule handles reactives with complex dependency tree", {
module <- function(input, output, session) {
x <- reactiveValues(x=1)
r <- reactive({
x$x + input$a + input$b
})
r2 <- reactive({
r() + input$c
})
}
testModule(module, {
session$setInputs(a=1, b=2, c=3)
expect_equal(r(), 4)
expect_equal(r2(), 7)
session$setInputs(a=2)
expect_equal(r(), 5)
expect_equal(r2(), 8)
session$setInputs(b=0)
expect_equal(r2(), 6)
expect_equal(r(), 3)
session$setInputs(c=4)
expect_equal(r(), 3)
expect_equal(r2(), 7)
})
})
test_that("testModule handles reactivePoll", {
module <- function(input, output, session) {
rv <- reactiveValues(x = 0)
rp <- reactivePoll(50, session, function(){ rnorm(1) }, function(){
isolate(rv$x <- rv$x + 1)
rnorm(1)
})
observe({rp()})
}
testModule(module, {
expect_equal(rv$x, 1)
for (i in 1:4){
session$elapse(50)
}
expect_equal(rv$x, 5)
})
})
test_that("testModule handles reactiveTimer", {
module <- function(input, output, session) {
rv <- reactiveValues(x = 0)
rp <- reactiveTimer(50)
observe({
rp()
isolate(rv$x <- rv$x + 1)
})
}
testModule(module, {
expect_equal(rv$x, 1)
session$elapse(200)
expect_equal(rv$x, 5)
})
})
test_that("testModule handles debounce/throttle", {
module <- function(input, output, session) {
rv <- reactiveValues(t = 0, d = 0)
react <- reactive({
input$y
})
rt <- throttle(react, 100)
rd <- debounce(react, 100)
observe({
rt() # Invalidate this block on the timer
isolate(rv$t <- rv$t + 1)
})
observe({
rd()
isolate(rv$d <- rv$d + 1)
})
}
testModule(module, {
session$setInputs(y = TRUE)
expect_equal(rv$d, 1)
for (i in 2:5){
session$setInputs(y = FALSE)
session$elapse(51)
session$setInputs(y = TRUE)
expect_equal(rv$t, i-1)
session$elapse(51) # TODO: we usually don't have to pad by a ms, but here we do. Investigate.
expect_equal(rv$t, i)
}
# Never sufficient time to debounce. Not incremented
expect_equal(rv$d, 1)
session$elapse(50)
# Now that 100ms has passed since the last update, debounce should have triggered
expect_equal(rv$d, 2)
})
})
test_that("testModule wraps output in an observer", {
testthat::skip("I'm not sure of a great way to test this without timers.")
# And honestly it's so foundational in what we're doing now that it might not be necessary to test?
module <- function(input, output, session) {
rv <- reactiveValues(x=0)
rp <- reactiveTimer(50)
output$txt <- renderText({
rp()
isolate(rv$x <- rv$x + 1)
})
}
testModule(module, {
session$setInputs(x=1)
# Timers only tick if they're being observed. If the output weren't being
# wrapped in an observer, we'd see the value of rv$x initialize to zero and
# only increment when we evaluated the output. e.g.:
#
# expect_equal(rv$x, 0)
# Sys.sleep(1)
# expect_equal(rv$x, 0)
# output$txt()
# expect_equal(rv$x, 1)
expect_equal(rv$x, 1)
expect_equal(output$txt, "1")
Sys.sleep(.05)
Sys.sleep(.05)
expect_gt(rv$x, 1)
expect_equal(output$txt, as.character(rv$x))
})
# FIXME:
# - Do we want the output to be accessible natively, or some $get() on the output? If we do a get() we could
# do more helpful spy-type things around exec count.
# - plots and such?
})
test_that("testModule works with async", {
module <- function(input, output, session) {
output$txt <- renderText({
val <- input$x
future({ val })
})
output$error <- renderText({
future({ stop("error here") })
})
output$sync <- renderText({
# No promises here
"abc"
})
}
testModule(module, {
session$setInputs(x=1)
expect_equal(output$txt, "1")
expect_equal(output$sync, "abc")
# Error gets thrown repeatedly
expect_error(output$error, "error here")
expect_error(output$error, "error here")
# Responds reactively
session$setInputs(x=2)
expect_equal(output$txt, "2")
# Error still thrown
expect_error(output$error, "error here")
})
})
test_that("testModule works with multiple promises in parallel", {
module <- function(input, output, session) {
output$txt1 <- renderText({
future({
Sys.sleep(1)
1
})
})
output$txt2 <- renderText({
future({
Sys.sleep(1)
2
})
})
}
testModule(module, {
# As we enter this test code, the promises will still be running in the background.
# We'll need to give them ~2s (plus overhead) to complete
startMS <- as.numeric(Sys.time()) * 1000
expect_equal(output$txt1, "1") # This first call will block waiting for the promise to return
expect_equal(output$txt2, "2")
expect_equal(output$txt2, "2") # Now that we have the values, access should not incur a 1s delay.
expect_equal(output$txt1, "1")
expect_equal(output$txt1, "1")
expect_equal(output$txt2, "2")
endMS <- as.numeric(Sys.time()) * 1000
# We'll pad quite a bit because promises can introduce some lag. But the point we're trying
# to prove is that we're not hitting a 1s delay for each output access, which = 6000ms. If we're
# under that, then things are likely working.
expect_lt(endMS - startMS, 4000)
})
})
test_that("testModule handles async errors", {
module <- function(input, output, session, arg1, arg2){
output$err <- renderText({
future({ "my error"}) %...>%
stop() %...>%
print() # Extra steps after the error
})
output$safe <- renderText({
future({ safeError("my safe error") }) %...>%
stop()
})
}
testModule(module, {
expect_error(output$err, "my error")
# TODO: helper for safe errors so users don't have to learn "shiny.custom.error"?
expect_error(output$safe, "my safe error", class="shiny.custom.error")
})
})
test_that("testModule handles modules with additional arguments", {
module <- function(input, output, session, arg1, arg2){
output$txt1 <- renderText({
arg1
})
output$txt2 <- renderText({
arg2
})
output$inp <- renderText({
input$x
})
}
testModule(module, {
expect_equal(output$txt1, "val1")
expect_equal(output$txt2, "val2")
}, arg1="val1", arg2="val2")
})
test_that("testModule captures htmlwidgets", {
# TODO: use a simple built-in htmlwidget instead of something complex like dygraph
if (!requireNamespace("dygraphs")){
testthat::skip("dygraphs not available to test htmlwidgets")
}
if (!requireNamespace("jsonlite")){
testthat::skip("jsonlite not available to test htmlwidgets")
}
module <- function(input, output, session){
output$dy <- dygraphs::renderDygraph({
dygraphs::dygraph(data.frame(outcome=0:5, year=2000:2005))
})
}
testModule(module, {
# Really, this test should be specific to each htmlwidget. Here, we don't want to bind ourselves
# to the current JSON structure of dygraphs, so we'll just check one element to see that the raw
# JSON was exposed and is accessible in tests.
d <- jsonlite::fromJSON(output$dy)$x$data
expect_equal(d[1,], 0:5)
expect_equal(d[2,], 2000:2005)
})
})
test_that("testModule captures renderUI", {
module <- function(input, output, session){
output$ui <- renderUI({
tags$a(href="https://rstudio.com", "hello!")
})
}
testModule(module, {
expect_equal(output$ui$deps, list())
expect_equal(as.character(output$ui$html), "<a href=\"https://rstudio.com\">hello!</a>")
})
})
test_that("testModule captures base graphics outputs", {
module <- function(input, output, session){
output$fixed <- renderPlot({
plot(1,1)
}, width=300, height=350)
output$dynamic <- renderPlot({
plot(1,1)
})
}
testModule(module, {
# We aren't yet able to create reproducible graphics, so this test is intentionally pretty
# limited.
expect_equal(output$fixed$width, 300)
expect_equal(output$fixed$height, 350)
expect_match(output$fixed$src, "^data:image/png;base64,")
# Ensure that the plot defaults to a reasonable size.
expect_equal(output$dynamic$width, 600)
expect_equal(output$dynamic$height, 400)
expect_match(output$dynamic$src, "^data:image/png;base64,")
# TODO: how do you customize automatically inferred plot sizes?
# session$setPlotMeta("dynamic", width=600, height=300) ?
})
})
test_that("testModule captures ggplot2 outputs", {
if (!requireNamespace("ggplot2")){
testthat::skip("ggplot2 not available")
}
module <- function(input, output, session){
output$fixed <- renderPlot({
ggplot2::qplot(iris$Sepal.Length, iris$Sepal.Width)
}, width=300, height=350)
output$dynamic <- renderPlot({
ggplot2::qplot(iris$Sepal.Length, iris$Sepal.Width)
})
}
testModule(module, {
expect_equal(output$fixed$width, 300)
expect_equal(output$fixed$height, 350)
expect_match(output$fixed$src, "^data:image/png;base64,")
# Ensure that the plot defaults to a reasonable size.
expect_equal(output$dynamic$width, 600)
expect_equal(output$dynamic$height, 400)
expect_match(output$dynamic$src, "^data:image/png;base64,")
})
})
test_that("testModule exposes the returned value from the module", {
module <- function(input, output, session){
reactive({
return(input$a + input$b)
})
}
testModule(module, {
session$setInputs(a=1, b=2)
expect_equal(session$returned(), 3)
# And retains reactivity
session$setInputs(a=2)
expect_equal(session$returned(), 4)
})
})
test_that("testModule handles synchronous errors", {
module <- function(input, output, session, arg1, arg2){
output$err <- renderText({
stop("my error")
})
output$safe <- renderText({
stop(safeError("my safe error"))
})
}
testModule(module, {
expect_error(output$err, "my error")
# TODO: helper for safe errors so users don't have to learn "shiny.custom.error"?
expect_error(output$safe, "my safe error", class="shiny.custom.error")
})
})
test_that("accessing a non-existant output gives an informative message", {
module <- function(input, output, session){}
testModule(module, {
expect_error(output$dontexist, "hasn't been defined yet: output\\$dontexist")
})
})
test_that("testModule works with nested modules", {
outerModule <- function(input, output, session) {
r1 <- reactive({ input$x + 1})
r2 <- callModule(innerModule, "innerModule", r1)
output$someVar <- renderText(r2())
}
innerModule <- function(input, output, session, r) {
reactive(paste("a value:", r()))
}
testModule(outerModule, {
session$setInputs(x = 1)
expect_equal(output$someVar, "a value: 2")
})
})
test_that("assigning an output in a module function with a non-function errors", {
module <- function(input, output, session) {
output$someVar <- 123
}
expect_error(testModule(module, {}), "^Unexpected")
})
test_that("testServer works", {
# app.R
testServer({
session$setInputs(dist="norm", n=5)
expect_length(d(), 5)
session$setInputs(dist="unif", n=6)
expect_length(d(), 6)
}, appDir=test_path("..", "test-modules", "06_tabsets"))
# server.R
testServer({
session$setInputs(dist="norm", n=5)
expect_length(d(), 5)
session$setInputs(dist="unif", n=6)
expect_length(d(), 6)
}, appDir=test_path("..", "test-modules", "server_r"))
})
test_that("testServer works when referencing external globals", {
# If global is defined at the top of app.R outside of the server function.
testServer({
expect_equal(global, 123)
}, appDir=test_path("..", "test-modules", "06_tabsets"))
})
test_that("testModule handles invalidateLater", {
module <- function(input, output, session) {
rv <- reactiveValues(x = 0)
observe({
isolate(rv$x <- rv$x + 1)
# We're only testing one invalidation
if (isolate(rv$x) <= 1){
invalidateLater(50)
}
})
}
testModule(module, {
# Should have run once
expect_equal(rv$x, 1)
session$elapse(49)
expect_equal(rv$x, 1)
session$elapse(1)
# Should have been incremented now
expect_equal(rv$x, 2)
})
})
test_that("session ended handlers work", {
module <- function(input, output, session){}
testModule(module, {
rv <- reactiveValues(closed = FALSE)
session$onEnded(function(){
rv$closed <- TRUE
})
expect_equal(session$isEnded(), FALSE)
expect_equal(session$isClosed(), FALSE)
expect_false(rv$closed, FALSE)
session$close()
expect_equal(session$isEnded(), TRUE)
expect_equal(session$isClosed(), TRUE)
expect_false(rv$closed, TRUE)
})
})
test_that("session flush handlers work", {
module <- function(input, output, session) {
rv <- reactiveValues(x = 0, flushCounter = 0, flushedCounter = 0,
flushOnceCounter = 0, flushedOnceCounter = 0)
onFlush(function(){rv$flushCounter <- rv$flushCounter + 1}, once=FALSE)
onFlushed(function(){rv$flushedCounter <- rv$flushedCounter + 1}, once=FALSE)
onFlushed(function(){rv$flushOnceCounter <- rv$flushOnceCounter + 1}, once=TRUE)
onFlushed(function(){rv$flushedOnceCounter <- rv$flushedOnceCounter + 1}, once=TRUE)
observe({
rv$x <- input$x * 2
})
}
testModule(module, {
session$setInputs(x=1)
expect_equal(rv$x, 2)
# We're not concerned with the exact values here -- only that they increase
fc <- rv$flushCounter
fdc <- rv$flushedCounter
session$setInputs(x=2)
expect_gt(rv$flushCounter, fc)
expect_gt(rv$flushedCounter, fdc)
# These should have only run once
expect_equal(rv$flushOnceCounter, 1)
expect_equal(rv$flushedOnceCounter, 1)
})
})
test_that("findApp errors with no app", {
calls <- 0
nothingExists <- function(path){
calls <<- calls + 1
FALSE
}
fa <- rewire(findApp, file.exists.ci=nothingExists)
expect_error(
expect_warning(fa("/some/path/here"), "No such file or directory"), # since we just made up a path
"No shiny app was found in ")
expect_equal(calls, 4 * 2) # Checks here, path, some, and / -- looking for app.R and server.R for each
})
test_that("findApp works with app in current or parent dir", {
calls <- 0
cd <- normalizePath(".")
mockExists <- function(path){
# Only TRUE if looking for server.R or app.R in current Dir
calls <<- calls + 1
path <- normalizePath(path, mustWork = FALSE)
appPath <- normalizePath(file.path(cd, "app.R"), mustWork = FALSE)
serverPath <- normalizePath(file.path(cd, "server.R"), mustWork = FALSE)
return(path %in% c(appPath, serverPath))
}
fa <- rewire(findApp, file.exists.ci=mockExists)
expect_equal(fa(), cd)
expect_equal(calls, 1) # Should get a hit on the first call and stop
# Reset and point to the parent dir
calls <- 0
cd <- normalizePath("..") # TODO: won't work if running tests in the root dir.
f <- fa()
expect_equal(normalizePath(f, mustWork = FALSE), cd)
expect_equal(calls, 3) # Two for current dir and hit on the first in the parent
})
Add the following code to your website.
For more information on customizing the embed code, read Embedding Snippets.