Skip to content

Commit c22d0fd

Browse files
stefanedwardsStefan McKinnon Edwards
authored and
Stefan McKinnon Edwards
committed
add: verifyInputMessage for mock-session
New manner for testing the contents of `message` when unit testing methods that use `sendInputMessage`. Similar methods should be implemented for session$sendCustomMessage and session$sendBinaryMessage.
1 parent 4d05a56 commit c22d0fd

File tree

6 files changed

+260
-38
lines changed

6 files changed

+260
-38
lines changed

NAMESPACE

+1
Original file line numberDiff line numberDiff line change
@@ -403,6 +403,7 @@ importFrom(rlang,get_env)
403403
importFrom(rlang,get_expr)
404404
importFrom(rlang,inject)
405405
importFrom(rlang,is_false)
406+
importFrom(rlang,is_function)
406407
importFrom(rlang,is_missing)
407408
importFrom(rlang,is_na)
408409
importFrom(rlang,is_quosure)

NEWS.md

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
* `Map` objects are now initialized at load time instead of build time. This avoids potential problems that could arise from storing `fastmap` objects into the built Shiny package. (#3775)
1414

15+
* Added methods to `mock-session` for verifying that specific calls to `session$sendInputMessage` were performed; see `verifyInputMessage`. Fully supports unit testing with the `testthat`-package, but does not require it.
16+
1517
### Bug fixes
1618

1719
* Fixed #3771: Sometimes the error `ion.rangeSlider.min.js: i.stopPropagation is not a function` would appear in the JavaScript console. (#3772)

R/mock-session.R

+93-2
Original file line numberDiff line numberDiff line change
@@ -149,7 +149,6 @@ makeExtraMethods <- function() {
149149
"sendBinaryMessage",
150150
"sendChangeTabVisibility",
151151
"sendCustomMessage",
152-
"sendInputMessage",
153152
"sendInsertTab",
154153
"sendInsertUI",
155154
"sendModal",
@@ -209,6 +208,7 @@ addGeneratedInstanceMethods <- function(instance, methods = makeExtraMethods())
209208
#' of [testServer()].
210209
#'
211210
#' @include timer.R
211+
#' @importFrom rlang is_function
212212
#' @export
213213
MockShinySession <- R6Class(
214214
'MockShinySession',
@@ -609,6 +609,95 @@ MockShinySession <- R6Class(
609609
getCurrentOutputInfo = function() {
610610
name <- private$currentOutputName
611611
if (is.null(name)) NULL else list(name = name)
612+
},
613+
614+
#' @description
615+
#' Mocks a `session$sendInputMessage`-call
616+
#' that can be later verified.
617+
#' @param inputId,message See `sendInputMessage` in [session].
618+
sendInputMessage = function(inputId, message) {
619+
stopifnot(length(inputId) == 1) ## purely guessing on internal workings of session
620+
private$inputMessage[[as.character(inputId)]] = message
621+
},
622+
623+
#' @description
624+
#' Verifies that a call to `session$sendInputMessage` has been performed.
625+
#'
626+
#' Use either simple expectations, e.g. `expect_equal(., "some value")`,
627+
#' or functions, `function(x) is.list(x)` or
628+
#' `function(x) expect_equal(x, list(1))`.
629+
#'
630+
#' For simple expectations, the sent message is accessed with `.`.
631+
#'
632+
#' For functions, they are called with the sent message as first argument.
633+
#' If any of the expressions in the function throws an error, `verifyInputMessage`
634+
#' fails.
635+
#'
636+
#' For both functions and expectations, their returned value must be
637+
#' `NULL` or pass [`isTruthy`] for the assertion to succeed.
638+
#'
639+
#' NB! testthat's `expect_*`-functions, when the expectations succeeds,
640+
#' returns the tested value. I.e. if testing for any of the values on the
641+
#' list in [`isTruthy`] (`FALSE`, `""`, `vector(0)`, etc.), `verifyInputMessage`
642+
#' will fail if results not properly wrapped.
643+
#'
644+
#' @examples
645+
#' session <- MockShinySession$new()
646+
#' session$sendInputMessage("foo", "")
647+
#' session$sendInputMessage("bar", list(value=2, add=TRUE))
648+
#' session$verifyInputMessage("foo", . == "")
649+
#'\dontrun{
650+
#' # This should be wrapped in an if (requireNamespace("testthat)),
651+
#' # but expect_equal was still now found?!
652+
#' session$verifyInputMessage("bar", expect_equal(., list(value=2, add=TRUE)))
653+
#'
654+
#' # Will fail, as `expect_equal` returns the value, which
655+
#' # in this case is not truthy.
656+
#' session$verifyInputMessage("foo", expect_equal(., ""))
657+
#' }
658+
#'
659+
#' @param inputId Expected inputId and message of the
660+
#' last call to `session$sendInputMessage`.
661+
#' @param ... Assertions to test against.
662+
#' @param env (advanced use only) the environment in which to evaluate
663+
#' `...` assertions.
664+
verifyInputMessage = function(inputId, ..., env = rlang::caller_env()) {
665+
asserts <- eval(substitute(alist(...)))
666+
test.env <- new.env(parent = env)
667+
msg <- private$inputMessage[[as.character(inputId)]]
668+
if (length(msg) == 0) {
669+
stop(errorCondition(
670+
sprintf("session$sendInputMessage(inputId=\"%s\") has not been called.", inputId),
671+
class = c("failure","expectation")
672+
))
673+
}
674+
675+
delayedAssign(".", msg, assign.env = test.env)
676+
for (assertion in asserts) {
677+
res <- tryCatch({
678+
val <- eval(assertion, test.env)
679+
if (is_function(val)) {
680+
val <- val(msg)
681+
}
682+
outcome <- isTruthy(val %||% TRUE)
683+
attr(outcome, "msg") <- attr(val, "msg")
684+
outcome
685+
}, assertError = function(e) {
686+
structure(FALSE, msg = e$message)
687+
}, error = function(e) {
688+
stop(e)
689+
})
690+
if (!res) {
691+
msg <- attr(res, "msg") %||% paste0(deparse(assertion), " is not TRUE")
692+
stop(errorCondition(msg, class = c("failure", "expectation")))
693+
}
694+
}
695+
696+
# signal a (expectation?) condition, so testthat accepts this as a test.
697+
cond <- simpleCondition(TRUE)
698+
class(cond) <- c('expectation_success','expectation', class(cond))
699+
withRestarts(signalCondition(cond), continue_test = function(e) NULL)
700+
invisible(cond)
612701
}
613702
),
614703
private = list(
@@ -696,7 +785,9 @@ MockShinySession <- R6Class(
696785
createVarPromiseDomain(private, "currentOutputName", name),
697786
expr
698787
)
699-
}
788+
},
789+
790+
inputMessage = list()
700791
),
701792
active = list(
702793
#' @field files For internal use only.

man/MockShinySession.Rd

+102-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tests/testthat/test-mock-session.R

+45-1
Original file line numberDiff line numberDiff line change
@@ -245,7 +245,51 @@ test_that("session supports sendBinaryMessage", {
245245
test_that("session supports sendInputMessage", {
246246
session <- MockShinySession$new()
247247
session$sendInputMessage(inputId=1, message=2)
248-
expect_true(TRUE) # testthat insists that every test must have an expectation
248+
session$sendInputMessage(inputId="foo", message=list(bar=1, add=TRUE))
249+
session$verifyInputMessage(1, expect_equal(., 2))
250+
session$verifyInputMessage(1, function(x) {
251+
expect_type(x, "double")
252+
expect_equal(x, 2)
253+
})
254+
session$verifyInputMessage("foo", expect_true(.$add), expect_equal(.$bar, 1))
255+
})
256+
257+
test_that("verifyInputMessage is itself enough for a `test_that`", {
258+
session <- MockShinySession$new()
259+
session$sendInputMessage(inputId=1, message=2)
260+
session$verifyInputMessage(1, . == 2)
261+
})
262+
263+
test_that("session supports failing verifyInputMessage", {
264+
session <- MockShinySession$new()
265+
expect_failure(
266+
session$verifyInputMessage(1, expect_equal(., 1)),
267+
message = "session$sendInputMessage(inputId=\"1\") has not been called.",
268+
fixed = TRUE
269+
)
270+
session$sendInputMessage(inputId=1, message=2)
271+
expect_success(session$verifyInputMessage(1, expect_equal(., 2)))
272+
expect_failure(
273+
session$verifyInputMessage(1, expect_equal(., 1)),
274+
message = "`.` (`actual`) not equal to 1 (`expected`)",
275+
fixed = TRUE
276+
)
277+
expect_failure(
278+
session$verifyInputMessage(1, function(x) expect_equal(x, 1)),
279+
message = "`x` (`actual`) not equal to 1 (`expected`)",
280+
fixed = TRUE
281+
)
282+
expect_failure(
283+
session$verifyInputMessage(1, . == 1),
284+
message = ". == 1 is not TRUE",
285+
fixed = TRUE
286+
)
287+
expect_failure(
288+
session$verifyInputMessage(1, function(x) x == 1),
289+
message = "function(x) x == 1 is not TRUE",
290+
fixed = TRUE
291+
)
292+
249293
})
250294

251295
test_that("session supports setBookmarkExclude", {

tests/testthat/test-update-input.R

+17-34
Original file line numberDiff line numberDiff line change
@@ -1,36 +1,19 @@
11
test_that("Radio buttons and checkboxes work with modules", {
2-
createModuleSession <- function(moduleId) {
3-
session <- as.environment(list(
4-
ns = NS(moduleId),
5-
sendInputMessage = function(inputId, message) {
6-
session$lastInputMessage = list(id = inputId, message = message)
7-
}
8-
))
9-
class(session) <- "ShinySession"
10-
session
11-
}
12-
13-
sessA <- createModuleSession("modA")
14-
15-
updateRadioButtons(sessA, "test1", label = "Label", choices = letters[1:5])
16-
resultA <- sessA$lastInputMessage
17-
18-
expect_equal("test1", resultA$id)
19-
expect_equal("Label", resultA$message$label)
20-
expect_equal("a", resultA$message$value)
21-
expect_true(grepl('"modA-test1"', resultA$message$options))
22-
expect_false(grepl('"test1"', resultA$message$options))
23-
24-
25-
sessB <- createModuleSession("modB")
26-
27-
updateCheckboxGroupInput(sessB, "test2", label = "Label", choices = LETTERS[1:5])
28-
resultB <- sessB$lastInputMessage
29-
30-
expect_equal("test2", resultB$id)
31-
expect_equal("Label", resultB$message$label)
32-
expect_null(resultB$message$value)
33-
expect_true(grepl('"modB-test2"', resultB$message$options))
34-
expect_false(grepl('"test2"', resultB$message$options))
35-
2+
session <- MockShinySession$new()
3+
4+
updateRadioButtons(session, "test1", label = "Label", choices = letters[1:5])
5+
session$verifyInputMessage("test1",
6+
expect_equal(.$label, "Label"),
7+
expect_equal(.$value, "a"),
8+
expect_true(grepl('"mock-session-test1"', .$options)),
9+
!expect_false(grepl('"test1"', .$options)) ## negate returned FALSE from expect_false
10+
)
11+
12+
updateCheckboxGroupInput(session, "test2", label = "Label", choices = LETTERS[1:5])
13+
session$verifyInputMessage("test2",
14+
expect_equal(.$label, "Label"),
15+
expect_null(.$value),
16+
expect_true(grepl('"mock-session-test2"', .$options)),
17+
!expect_false(grepl('"test2"', .$options))
18+
)
3619
})

0 commit comments

Comments
 (0)