DEV Community

Cover image for Setting values in R6 classes, and testing with shiny::MockShinySession
Colin Fay
Colin Fay

Posted on • Originally published at rtask.thinkr.fr

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)
Enter fullscreen mode Exit fullscreen mode

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,
      "myemail@company.com"
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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" = "myemail@company.com"
        )
      )
    },
    overwrite = TRUE
  )
Enter fullscreen mode Exit fullscreen mode

Now, the session behaves as expected:

> session <- MockShinySession$new()
> session$request
$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
[1] "myemail@company.com
Enter fullscreen mode Exit fullscreen mode

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" = "myemail@company.com"
        )
      )
    },
    overwrite = TRUE
  )

  testServer(app_server, {
    # Waiting for the session to be fired up
    session$elapse(1)

    expect_equal(
      r$email,
      "myemail@company.com"
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

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" = "myemail@company.com"
        )
      )
    },
    overwrite = TRUE
  )

  testServer(app_server, {
    # Waiting for the session to be fired up
    session$elapse(1)

    expect_equal(
      r$email,
      "myemail@company.com"
    )
  })
})
Enter fullscreen mode Exit fullscreen mode

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!

Top comments (0)