Définir des valeurs dans les classes R6 et tester avec shiny::MockShinySession

Author : Colin Fay
Categories : développement, orienté-objet, shiny
Tags : R6, shiny, test
Date :

Contexte

Récemment, nous avons travaillé sur les tests d’une application {shiny} qui repose sur des valeurs stockées dans l’objet session$request. Cet objet est un environnement qui capture les détails de l’échange HTTP entre R et le navigateur. Sans trop entrer dans les détails techniques (même si cela me tente 😅), il est important de comprendre que session$request contient des informations fournies à la fois par le navigateur et par tout proxy redirigeant les requêtes.

Notre application est déployée derrière un proxy dans un environnement Microsoft Azure. Ici, le service d’authentification ajoute plusieurs en-têtes pour valider l’identité des utilisateurs (voir la documentation pour plus de détails). Des en-têtes comme X-MS-CLIENT-PRINCIPAL et X-MS-CLIENT-PRINCIPAL-ID sont essentiels pour identifier les utilisateurs, et l’application {shiny} en dépend pour gérer l’authentification.

Tester les en-têtes

Lorsque qu’un utilisateur se connecte à l’application, ses identifiants sont extraits d’un en-tête et stockés pour être utilisés dans toute l’application. Voici un exemple simplifié de ce fonctionnement :

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("Bonjour %s", r$email)
  })
}

shinyApp(ui, server)

Tester cette fonctionnalité, notamment dans des environnements d’intégration continue (CI), peut être un défi.

Dans notre cas, nous aimerions avoir quelque chose comme ceci :

test_that("app server", {

  # Ajuster la session ici

  testServer(app_server, {
    # Attendre que la session soit initialisée
    session$elapse(1)

    expect_equal(
      r$email,
      "[email protected]"
    )
  })
})

Cependant, les en-têtes d’authentification comme HTTP_X_MS_CLIENT_PRINCIPAL_NAME sont absents lors des tests automatisés (notamment sur le CI), nous devons donc trouver un moyen de simuler leur présence. Le package {shiny} fournit la classe MockShinySession pour les tests, mais elle ne simule pas nativement un objet session$request réaliste. Explorons comment contourner cette limitation.

Redéfinir session$request

Nous avons d’abord essayé de modifier directement session$request, mais cela ne fonctionne pas :

> session <- MockShinySession$new()
> session$request
<environment: 0x13a032600>
Warning message:
In (function (value)  :
  session$request doesn't currently simulate a realistic request on MockShinySession

Peut-être pouvons-nous ajouter une nouvelle entrée ?

> 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

Aïe, ça ne fonctionne pas. Continuons à explorer. Qu’est-ce que session ?

> class(session)
[1] "MockShinySession" "R6"
> class(session$request)
[1] "environment"

Comme on peut le voir, c’est un objet R6, une instance de la classe MockShinySession, et session$request est un environnement. Nous voulons accéder, dans notre application, à session$request$HTTP_X_MS_CLIENT_PRINCIPAL_NAME. Peut-être pourrions-nous redéfinir request ?

request se trouve dans le champ active de la classe R6 :

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

Pour redéfinir l’objet request, nous pouvons utiliser la méthode set() de la classe R6. Voici comment redéfinir le comportement :

MockShinySession$set(
    "active",
    "request",
    function(value) {
      return(
        list(
          "HTTP_X_MS_CLIENT_PRINCIPAL_NAME" = "[email protected]"
        )
      )
    },
    overwrite = TRUE
  )

Désormais, la session fonctionne comme attendu :

> session <- MockShinySession$new()
> session$request
$HTTP_X_MS_CLIENT_PRINCIPAL_NAME
[1] "[email protected]"

Écrire le test

Avec le request redéfini, nous pouvons maintenant écrire un test fonctionnel :

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, {
    session$elapse(1)
    expect_equal(
      r$email,
      "[email protected]"
    )
  })
})

Nettoyage après les tests

Une dernière chose : nous devons nettoyer notre test pour que l’objet session reste inchangé après son exécution. Pour cela, nous utilisons on.exit pour restaurer l’ancien comportement :

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, {
    session$elapse(1)
    expect_equal(
      r$email,
      "[email protected]"
    )
  })
})

Cette configuration garantit que nos tests restent isolés et fiables, même dans des environnements CI. En tirant parti de la flexibilité des objets R6, nous pouvons contrôler et simuler entièrement session$request pour tester les logiques dépendantes de l’authentification.

Besoin d’aide pour tester vos applications ?

Vous ne savez toujours pas comment mettre en place une stratégie de test efficace pour votre application ? Discutons-en !


Comments


Also read