Setting values in R6 classes, and testing with shiny::MockShinySession
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.
You can read the original post in its original format on Rtask website by ThinkR here: Setting values in R6 classes, and testing with shiny::MockShinySession
Context
Recently, we worked on testing a {shiny}
app that relies on values stored within the session$request
object. This object is an environment that captures the details of the HTTP exchange between R and the browser. Without diving too deeply into the technicalities (as much as I’d love to 😅), it’s important to understand that session$request
contains information provided by both the browser and any proxy redirecting the requests.
Our app is deployed behind a proxy in a Microsoft Azure environment. Here, the authentication service attaches several headers to validate user identity (see documentation for details). Headers like X-MS-CLIENT-PRINCIPAL
and X-MS-CLIENT-PRINCIPAL-ID
are critical for identifying users, and the {shiny}
app depends on these to manage authentication.
Testing headers
When a user connects to the app, their identifiers are retrieved from a header and stored for use throughout the app. Here’s a simplified example of how this might work:
library(shiny) ui <- fluidPage( textOutput("user_id") ) server <- function(input, output, session) { r <- reactiveValues( email = NULL ) observe({ r$email <- session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME }) output$user_id <- renderText({ req(r$email) sprintf("Hello %s", r$email) }) } shinyApp(ui, server)
Testing this functionality, particularly in Continuous Integration (CI) environments, can be challenging.
In our use case, we’d love to have something like this:
test_that("app server", { # Tweaking the session here testServer(app_server, { # Waiting for the session to be fired up session$elapse(1) expect_equal( r$email, "[email protected]" ) }) })
But Authentication headers like HTTP_X_MS_CLIENT_PRINCIPAL_NAME
are absent during automated tests, so we need a way to simulate their presence. {shiny}
provides the MockShinySession
class for testing, but it doesn’t natively simulate a realistic session$request
object. Let’s explore how to work around this limitation.
Overriding session$request
We first attempt to directly modify session$request
, but it doesn’t work:
> session <- MockShinySession$new() > session$request <environment: 0x13a032600> Warning message: In (function (value) : session$request doesn't currently simulate a realistic request on MockShinySession
Ok, maybe we can assign a new entry here?
> session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME <- "test" Error in (function (value) : session$request can't be assigned to In addition: Warning message: In (function (value) : session$request doesn't currently simulate a realistic request on MockShinySession
Ouch, it doesn’t work, it can’t be assigned to. But let’s continue our exploration. What is session
?
> class(session) [1] "MockShinySession" "R6" > class(session$request) [1] "environment"
As we can see, it’s an R6 object, an instance of the MockShinySession
class, and session$request
an env. What we want is being able to access, in our app, to session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
. Maybe we could override request
?
request
is contained in the active
field of the R6 class:
> MockShinySession$active # [...] $request function (value) { if (!missing(value)) { stop("session$request can't be assigned to") } warning("session$request doesn't currently simulate a realistic request on MockShinySession") new.env(parent = emptyenv()) } <bytecode: 0x11f25d8a8> <environment: namespace:shiny
To override the request
object, we can use the set()
method of the R6 class. Here’s how we redefine the behavior:
MockShinySession$set( "active", "request", function(value) { return( list( "HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]" ) ) }, overwrite = TRUE )
Now, the session behaves as expected:
> session <- MockShinySession$new() > session$request $HTTP_X_MS_CLIENT_PRINCIPAL_NAME [1] "[email protected]
Writing the Test
With the overridden request
, we can now write a functional test:
test_that("app server", { MockShinySession$set( "active", "request", function(value) { return( list( "HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]" ) ) }, overwrite = TRUE ) testServer(app_server, { # Waiting for the session to be fired up session$elapse(1) expect_equal( r$email, "[email protected]" ) }) })
Cleaning Up After Tests
But, just one more thing: we need to clean our test so that the session object stays the same after our test. For this, we’ll use on.exit
to restore the old behavior:
test_that("app server", { old_request <- MockShinySession$active$request on.exit({ MockShinySession$set( "active", "request", old_request, overwrite = TRUE ) }) MockShinySession$set( "active", "request", function(value) { return( list( "HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]" ) ) }, overwrite = TRUE ) testServer(app_server, { # Waiting for the session to be fired up session$elapse(1) expect_equal( r$email, "[email protected]" ) }) })
This setup ensures that our tests remain isolated and reliable, even in CI environments. By leveraging R6’s flexibility, we can fully control and mock session$request
to test authentication-dependent logic.
If you want to dig more into the details, you can visit this repo, where you’ll find a reproducible example!
Do you need help with testing your apps?
Still unsure how to implement a good testing strategy for your app? Let’s chat!
This post is better presented on its original ThinkR website here: Setting values in R6 classes, and testing with shiny::MockShinySession
R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.
Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.