Du laboratoire à la vraie vie : Comment votre application Shiny peut survivre à ses utilisateurs

talk about tests
Author : Arthur Bréant
Categories : développement, golem, golem, javascript, shiny
Tags : golem, shiny, Tests
Date :

Du prototype à la production, assurez-vous que rien ne se casse…

Vous avez réalisé une superbe maquette et votre client est ravi. Vous êtes prêt à passer à la production de votre application. Mais une question vous taraude : comment vous assurer que votre application restera stable et fonctionnelle au fil des modifications et des évolutions ?

La réponse tient en un mot : les tests.

Il y a trois semaines, une partie de l’équipe de ThinkR était en Belgique pour participer aux Rencontres R 2025. Une conférence autour de R dont l’objectif est d’offrir à la communauté francophone un lieu d’échange et de partage d’idées sur l’usage du langage R toutes disciplines confondues.

Mardi, dans l’après-midi, pendant une présentation de 15 minutes, je présentais l’utilisation des tests dans une application Shiny (présentation disponible en anglais et en français). Après plusieurs heures de préparations et une présentation, mon esprit était alors test-oriented. À la fin des 3 jours de conférence, je vais m’adonner à mon second hobby (après le développement dans R) : le running. Alors que je courais, une musique passe dans mes oreilles : Let’s Talk About Sex de Salt-N-Pepa. Je n’entends alors que : “Let’s talk about you and me […] Let’s talk about tests”. Déformation professionnelle ? Sûrement.

Musique à fond, Let’s talk about tests !

Yo, I don’t think we should talk about this
Come on, why not ?
People might misunderstand what we’re tryin’ to say, you know ?
No, but that’s a part of life
[…]
Let’s talk about [tests], baby

Trop souvent négligés dans l’écosystème R, les tests sont pourtant essentiels pour garantir la robustesse et la pérennité de vos applications Shiny. Dans cet article, nous explorerons une stratégie de tests à trois niveaux qui vous permettra de sécuriser votre code du développement à la production.

Pourquoi tester ses applications Shiny ?

Le développement d’une application Shiny suit souvent le même parcours : vous commencez par un prototype rapide, vous itérez avec votre client (ou vous-même), vous ajoutez des fonctionnalités… et soudain, vous réalisez que votre code est devenu complexe et fragile.

Chaque nouvelle modification risque de casser une fonctionnalité existante. Chaque ajout de feature vous fait craindre d’introduire des régressions. C’est là que les tests deviennent votre meilleur allié.

Les avantages d’une stratégie de tests

Une stratégie de tests bien pensée vous permet de :

  • Valider progressivement votre code à chaque étape du développement
  • Détecter les régressions avant qu’elles n’atteignent vos utilisateurs
  • Refactoriser en toute sérénité en sachant que vos tests vous alerteront en cas de problème
  • Documenter le comportement attendu de votre application
  • Faciliter la maintenance et les évolutions futures
Vue d’ensemble

Pour sécuriser efficacement une application Shiny, nous recommandons une approche à trois niveaux :

  1. Tests unitaires : Valider chaque fonction individuellement
  2. Tests d’intégration : Vérifier les interactions entre composants
  3. Tests End-to-End : Simuler un utilisateur réel dans un –vrai– navigateur

e2e tests

 

Chaque niveau a son rôle et ses spécificités. Ensemble, ils forment un filet de sécurité complet pour votre application.

Niveau 1 : Les tests unitaires

Le principe

Les tests unitaires consistent à tester chaque fonction de manière isolée, indépendamment du reste de l’application.

C’est comme vérifier qu’un tiroir s’ouvre correctement avant de l’installer dans une cuisine.

 

Unit test

Mise en pratique

Si vous suivez les bonnes pratiques et développez votre application Shiny sous forme de package (avec {golem}), les tests unitaires s’intègrent naturellement dans votre workflow.

Créons notre première application :

# install.packages("golem")
golem::create_golem(path = "myShinyApp")

Une fois ce code exécuté, votre package va s’ouvrir dans une nouvelle session. Vous pouvez immédiatement vérifier que votre application se lance correctement :

# Lancement de l'application en mode dev
golem::run_dev()

Dans {golem}, le cycle de développement se trouve dans le dossier /dev. Vous y trouverez l’ensemble des fonctions dont vous aurez besoin pour le développement.

Le premier fichier à suivre est le fichier dev/01_start.R. Il contient l’ensemble des fonctions dont vous aurez besoin pour démarrer votre projet. Parmi elles se trouve la fonction : golem::use_recommended_tests(). Cette fonction va créer la structure de tests dans votre package, avec des tests recommandés par {golem} dont un vérifie que votre application va se lancer correctement. Pratique !

Imaginons que nous avons besoin dans notre application d’une fonction : calculate_average(). On peut exécuter : usethis::use_r("calculate_average") pour créer le fichier qui va contenir notre fonction.

# Dans R/calculate_average.R 
calculate_average <- function(values) { 
    if (!is.numeric(values)) { 
        stop("values doit être numérique") 
    } 
    if (length(values) == 0) { 
        return(0) 
    } 
    sum(values) / length(values) 
}

Cette fonction permet de calculer une moyenne. Elle possède quelques “vérificateurs” pour checker les entrées de la fonction. Pour lui associer un test unitaire, on peut exécuter : usethis::use_test(name = "calculate_average")

# Dans tests/testthat/test-calculate_average.R
test_that("calculate_average fonctionne correctement", {
  # Test avec des valeurs numériques
  expect_equal(
    object = calculate_average(
      values = c(10, 20, 30)
    ),
    expected = 20
  )
  # Test avec un vecteur vide
  expect_equal(
    object = calculate_average(
      values = 0
    ),
    expected = 0
  )
  # Test avec input non-numérique
  expect_error(
    object = calculate_average(
      values = c("a", "b")
    ),
    "values doit être numérique"
  )
})

Pour vérifier que la fonctionne calculate_average fonctionne comme prévu, on exécute les tests :

devtools::test()

Si les tests sont concluants, on obtient :

test result 1

Vous pouvez modifier / casser un test pour voir et expérimenter un test en échec !
Remplacer donc calculate_average(values = c(10, 20, 30)) par calculate_average(values = c(10, 20, 1)) pour voir le résultat.

À cette étape, les tests unitaires permettent de tester les fonctions métiers de notre application. On ne vient pas tester à proprement parler l’application mais on s’assure un bon comportement de sa logique métier.

Niveau 2 : Les tests d’intégration

Le principe

Les tests d’intégration vérifient que les différents composants de votre application fonctionnent correctement ensemble. Dans Shiny, cela signifie tester les flux réactifs, les interactions entre modules, et la logique serveur.

On a vérifié si le tiroir pouvait s’ouvrir correctement. On va vérifier maintenant qu’il s’intègre correctement avec le reste des meubles de la cuisine : les autres tiroirs, le plan de travail, etc…

Integration tests

 

Mise en pratique

Modifions un peu notre application !

Dans le fichier R/app_ui.R, remplacez la fonction golem::golem_welcome_page() par le code suivant :

numericInput(inputId = "num1", label = "Première valeur", value = 10), 
numericInput(inputId = "num2", label = "Seconde valeur", value = 10), 
numericInput(inputId = "num3", label = "Troisième valeur", value = 10), 
numericInput(inputId = "num4", label = "Quatrième valeur", value = 10), 
actionButton(inputId = "go", label = "Calculer !"), 
textOutput(outputId = "result")

Cela doit donner :

app_ui <- function(request) {
  tagList(
    golem_add_external_resources(),
    fluidPage(
      numericInput(inputId = "num1", label = "Première valeur", value = 10), 
      numericInput(inputId = "num2", label = "Seconde valeur", value = 10), 
      numericInput(inputId = "num3", label = "Troisième valeur", value = 10), 
      numericInput(inputId = "num4", label = "Quatrième valeur", value = 10), 
      actionButton(inputId = "go", label = "Calculer !"), 
      textOutput(outputId = "result")
    )
  )
}

Nous venons d’ajouter 5 inputs dont 4 inputs pour saisir une valeur numérique et un bouton d’action. Enfin un textOutput permettra d’afficher du texte.

Attention à bien mettre une virgule entre les différents éléments de votre UI.

Pour vérifier que ce code fonctionne, on peut lancer l’application :

# Lancement de l'application en mode dev
golem::run_dev()

Il est temps de connecter la partie UI de notre application avec la partie serveur. Pour cela, dans R/app_server.R :

app_server <- function(input, output, session) {
  rv <- reactiveValues()
  observeEvent(input$go, {
    rv$avg <- calculate_average(
      c(input$num1, input$num2, input$num3, input$num4)
    )
    showNotification(
      "Calcul réalisé !",
      duration = 3
    )
  })
  output$result <- renderText({
    req(rv$avg)
    paste("Moyenne :", rv$avg)
  })
}

Ce code vient de créer une reactiveValues(), une boîte qui va stocker les différents résultats dans notre application. Lorsque le bouton sera cliqué, le code à l’intérieur de l’observeEvent sera exécuté. On stocke dans la reactiveValues le résultat de notre fonction calculate_average.

golem::run_dev()

La fonction testServer()

Shiny fournit la fonction testServer() qui permet de tester la logique serveur sans lancer l’interface utilisateur.

Dans un nouveau fichier usethis::use_test(name = "server"), copiez le code suivant :

testServer(app_server, {
  session$setInputs(num1 = 5)
  session$setInputs(num2 = 5)
  session$setInputs(num3 = 5)
  session$setInputs(num4 = 5)
  session$setInputs(go = 1)
  expect_equal(
    object = rv$avg,
    expected = 5
  )
  session$setInputs(num1 = 10)
  session$setInputs(num2 = 20)
  session$setInputs(num3 = 30)
  session$setInputs(num4 = 12)
  session$setInputs(go = 2)
  expect_equal(
    object = rv$avg,
    expected = 18
  )
})

Ce test simule dans le serveur des valeurs pour les différents inputs et simule également un clic sur le bouton session$setInputs(go = 1).

On s’attend alors que le résultat stocké dans la reactiveValues soit de 5 :

expect_equal(
  object = rv$avg,
  expected = 5
)

Comme précédemment, pour faire tourner les tests dans notre application, on exécute :

devtools::test()

Result test 2

Vous pouvez toujours casser un test pour expérimenter un test en échec !
Remplacer donc session$setInputs(num3 = 30) par session$setInputs(num3 = 2) pour voir le résultat.

Cette logique de test d’intégration avec testServer peut également être utilisée avec des modules dans Shiny ! L’application complète et les plus complexes peuvent donc être testées.

Nous venons de tester les imbrications dans l’application et notamment les différentes interactions. Cependant, ceci reste très “programmatique” et ne reflète pas la réelle expérience d’un utilisateur dans un navigateur.

Niveau 3 : Les tests End-to-End

Le principe

Les tests End-to-End simulent un utilisateur réel interagissant avec votre application dans un vrai navigateur. C’est le niveau de test le plus proche de l’expérience utilisateur finale.

Les objectifs sont multiples ici :

  • Simuler de vraies interactions utilisateur
  • Tester l’application dans un vrai navigateur
  • Vérifier l’expérience utilisateur complète

On a vérifié les tiroirs individuellement. On a également vérifié qu’ils pouvaient s’assembler tous ensemble. Maintenant, il est temps de tester de cuisiner un vrai repas dans la cuisine !

e2e tests

Playwright et le package {pw}

Pour les tests E2E (End-to-End) dans l’écosystème R, nous recommandons l’utilisation de Playwright via le package {pw} développé par ThinkR.

Installation et configuration :

# Installation du package (en développement)
devtools::install_github("ThinkR-open/pw")
# Initialisation de la structure de tests
pw::pw_init()

Cette commande crée la structure suivante :

tests/
├── playwright/
    ├── tests/
        ├── default.test.ts
└── testthat/
    ├── test-calculate_average.R
    ├── test-golem-recommended.R
    ├── test-server.R
    └── test-playwright.R

Les tests E2E seront à ranger dans le dossier playwright/tests/. Un test est déjà disponible, il contient :

import { test, expect } from '@playwright/test';
test('has body', async ({ page }) => {
  await page.goto('http://127.0.0.1:3000');
  await expect(page.locator('body')).toBeVisible();
});

Ce code va lancer l’application dans un navigateur et vérifier la présence de notre body. Ce test, on peut déjà l’exécuter :

pw::pw_test()

Pw tests

Playwright propose également un rapport que nous pouvons consulter :

pw::pw_show_report()

Pw report

Playwright exécute ce test dans 3 navigateurs différents : chromium, firefox et webkit.

Ok, mais comment tester notre application ?

Bonne nouvelle, pas besoin d’apprendre nécessairement TypeScript pour produire des tests E2E avec Playwright :

pw::pw_codegen()

Cette fonction va ouvrir un navigateur, dans lequel nous allons pouvoir simuler des clics et des actions, en tant qu’utilisateur.

codegen 1

 

 

Codegen 2

 

 

Codegen 3

Codegen 4

Playwright va enregistrer nos actions et les stocker dans un nouveau fichier dans tests/playwright/tests :

import { test, expect } from '@playwright/test';
test('test', async ({ page }) => {
  await page.goto('http://localhost:3000/');
  await page.getByRole('spinbutton', { name: 'Première valeur' }).click();
  await page.getByRole('spinbutton', { name: 'Première valeur' }).fill('20');
  await page.getByText('Première valeur Seconde').click();
  await page.getByRole('button', { name: 'Calculer !' }).click();
  await page.getByText('Moyenne :').click();
});

Ce test est loin d’être parfait et nous reviendrons dans un prochain article sur la syntaxe des tests avec Playwright et comment les optimiser. Les tests E2E fonctionnent aussi dans le CI et nous reviendrons également dessus dans un prochain article.

En attendant, notre application ici est testée dans un contexte qui se rapproche le plus de la réalité de nos futurs utilisateurs. Nous venons de sécuriser le bon fonctionnement de l’application, tant sur la logique métier que sur la partie UI.

On peut lancer l’ensemble des tests dans notre application :

devtools::test()

Result tests 3

Stratégie de tests recommandée

Répartition des efforts

Une règle couramment admise dans l’industrie est la pyramide des tests :

  • 70% de tests unitaires : Rapides, fiables, nombreux
  • 20% de tests d’intégration : Focus sur les interactions critiques
  • 10% de tests E2E : Parcours utilisateur essentiels

Que tester à chaque niveau ?

Tests unitaires :

  • Toutes les fonctions utilitaires et de logique métier
  • Fonctions de transformation de données
  • Algorithmes de calcul
  • Fonctions de validation d’entrées

Tests d’intégration :

  • Flux réactifs principaux de l’application
  • Interactions entre modules Shiny
  • Logique serveur complexe
  • Mise à jour des reactive values

Tests End-to-End :

  • Parcours utilisateur critiques
  • Workflows complets (connexion, calcul, export)
  • Fonctionnalités complexes impliquant plusieurs interactions
  • Tests de régression sur les bugs majeurs

Conclusion

La mise en place d’une stratégie de tests à trois niveaux transforme radicalement la façon dont vous développez et maintenez vos applications Shiny.

Les bénéfices concrets :

  • Confiance dans votre code et vos modifications
  • Rapidité de détection des problèmes
  • Facilité de maintenance et d’évolution
  • Qualité utilisateur améliorée

Pour commencer dès aujourd’hui :

  1. Structurez votre application Shiny comme un package avec {golem}
  2. Ajoutez des tests unitaires pour vos fonctions critiques
  3. Implémentez quelques tests d’intégration avec testServer()
  4. Expérimentez avec les tests E2E via {pw} pour vos parcours utilisateur principaux

N’hésitez pas à nous contacter si vous souhaitez approfondir la mise en place de tests dans vos applications Shiny !

Vous pouvez également retrouver Colin Fay qui animera un workshop le mercredi 8 octobre 2025 lors de la conférence Shiny In Production organisé par Jumping Rivers.

Vous pouvez également retrouver toutes nos formations sur le développement d’applications Shiny ici !


Comments


Also read