IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

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

    Colin Fay发表于 2025-01-07 09:29:12
    love 0
    [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,
          "myemail@company.com"
        )
      })
    })
    
    

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

    Now, the session behaves as expected:

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

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

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

    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

    To leave a comment for the author, please follow the link and comment on their blog: Rtask.

    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.
    Continue reading: Setting values in R6 classes, and testing with shiny::MockShinySession


沪ICP备19023445号-2号
友情链接