Setting values in R6 classes, and testing with shiny::MockShinySession



[This article was first published on Rtask, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)


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

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())
}


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






Source link

Related Posts

About The Author

Add Comment