Tests et débogage Shiny : testthat, testServer et reactlog
Attrapez les régressions avec une suite de tests, prouvez la logique réactive sans navigateur avec testServer, et voyez exactement ce que fait votre graphe réactif avec reactlog et browser().
Testez et déboguez une application Shiny avec la boîte à outils du quotidien. Testez unitairement vos fonctions de logique métier avec testthat, testez la logique réactive et de module avec testServer (sans navigateur), visualisez le graphe réactif avec reactlog, parcourez un réactif pas à pas avec browser(), et transformez les traces d’appels en messages clairs avec req() et validate().
- Testez unitairement votre logique avec testthat. Déplacez les fonctions pures dans
R/, écrivez un bloctest_that()qui vérifie ce qu’elles renvoient, etdevtools::test()exécute la suite — les régressions sont attrapées par le code, pas par un utilisateur. - Testez la logique réactive sans navigateur avec
testServer(). Il pilote lesinput,outputet les réactifs d’une fonction serveur dans le processus, sans navigateur — définissez les entrées, lisez un réactif, vérifiez la valeur. C’est l’outil propre à Shiny. - Voyez le graphe réactif avec reactlog.
reactlog::reactlog_enable()avant de lancer l’application, puis Ctrl+F3 ouvre une carte en direct de chaque réactif, entrée et sortie — le moyen le plus rapide de répondre à « pourquoi cela s’est-il réexécuté ? » - Parcourez un réactif pas à pas avec
browser(). Placez-le dans un réactif ou un observateur pour mettre en pause et inspecter les valeurs à cet endroit précis ;options(shiny.error = browser)ouvre le débogueur dès qu’une erreur se déclenche. - The runtime is the judge.
testServer()exécute votre logique réactive et renvoie7sans aucun navigateur ouvert — copiez le bloc et exécutez-le pour prouver que la logique fonctionne avant même de lancer l’application.
Introduction
Vous avez corrigé une chose et cassé une autre, et vous ne l’avez découvert que lorsqu’un utilisateur l’a fait. Ou bien l’application réexécute une requête coûteuse à chaque frappe et vous n’arrivez pas à voir pourquoi. C’est le même problème dans les deux cas : une application Shiny a des rouages que vous ne voyez pas, et l’inspecter à l’œil ne passe pas à l’échelle.
Deux habitudes corrigent cela. Une suite de tests — des vérifications automatisées qui garantissent que votre code fait toujours ce qu’il doit — attrape les régressions dès l’instant où vous les introduisez. Et des outils de débogage réactif — reactlog et browser() — vous montrent ce que fait réellement le graphe réactif (le réseau de dépendances que Shiny construit entre les entrées, les réactifs et les sorties), au lieu de vous laisser deviner.
Cette leçon est la boîte à outils du quotidien : testez unitairement la logique métier, testez la logique réactive et de module sans navigateur, observez le graphe réactif, parcourez pas à pas un réactif qui se comporte mal, et transformez les vilaines traces d’appels en messages clairs. Elle correspond aux niveaux exposés par le chapitre sur les tests de Mastering Shiny.
Quand ne pas s’en soucier. Une application jetable de 30 lignes n’a pas besoin d’une suite — mais dès qu’elle acquiert une fonction utilitaire digne de confiance, ou une chaîne réactive que vous ne pouvez pas garder en tête, testez-la. Le coût d’un test se compte en minutes ; le coût d’une régression silencieuse en production, c’est la confiance d’un utilisateur.
Vous aurez besoin d’une application Shiny fonctionnelle avec quelques fonctions utilitaires et, idéalement, un module ou deux — les éléments qui valent la peine d’être testés. Installez les deux packages que cette leçon utilise :
install.packages(c("testthat", "reactlog"))testServer(), req() et validate() sont fournis avec Shiny lui-même, donc dès que vous avez Shiny, vous les avez déjà. La leçon sur la programmation réactive couvre les fondations réactives que tout ce qui suit présuppose.
Les tests et les outils de débogage ci-dessous s’exécutent dans une session R en direct, pas sur cette page — une page web statique ne peut ni lancer un serveur ni ouvrir un débogueur. Copiez n’importe quel bloc sur votre machine et exécutez-le en local. La preuve, c’est de l’exécuter : testServer() exécute votre logique réactive et renvoie la valeur affichée, et reactlog s’ouvre sur le vrai graphe de votre application. The runtime is the judge.
La pyramide des tests, en une ligne
Tous les tests ne se valent pas. Une application Shiny en bonne santé a de nombreux tests unitaires rapides à la base, quelques tests réactifs/serveur au milieu, et quelques tests de bout en bout au sommet — la classique pyramide des tests :
| Niveau | Ce qu’il teste | Outil | Combien |
|---|---|---|---|
| Unitaire | Fonctions pures — votre logique métier | testthat |
Beaucoup (rapides) |
| Réactif / serveur | Expressions réactives, serveurs de module | testServer() |
Quelques-uns |
| De bout en bout | L’application entière dans un vrai navigateur | shinytest2 |
Quelques-uns (lents) |
Plus un test se situe bas, plus il est rapide et fiable, c’est donc là que vous voulez l’essentiel de votre couverture. Cette leçon couvre les deux niveaux du bas — ceux que vous exécutez en permanence pendant le développement. Le niveau du haut, qui pilote l’interface réelle dans un navigateur sans affichage, est un sujet à part entière — shinytest2 s’en charge, et cela dépasse le cadre de cette leçon.
Testez unitairement votre logique métier
Les tests les plus précieux sont les moins coûteux : de simples vérifications sur les fonctions pures qui font le vrai travail de votre application. Un test unitaire vérifie qu’une fonction renvoie la bonne réponse pour une entrée donnée, de façon isolée — sans réactivité, sans navigateur.
La mise en place suit une convention de package. Placez une fonction pure dans R/ — disons, une fonction utilitaire qui calcule la moyenne des miles par gallon d’un data frame :
# R/summarise.R
summarise_mpg <- function(data) {
mean(data$mpg)
}Écrivez ensuite un test pour elle. Les tests résident dans tests/testthat/test-*.R, et chacun est un bloc test_that() avec une description et une ou plusieurs attentes :
# tests/testthat/test-summarise.R
test_that("summarise_mpg computes the mean", {
expect_equal(round(summarise_mpg(mtcars), 2), 20.09)
})expect_equal() vérifie que les deux valeurs correspondent ; si summarise_mpg() cesse un jour de renvoyer 20.09 pour mtcars, ce test échoue bruyamment. expect_true() et expect_error() sont les deux autres auxquelles vous ferez le plus appel — la dernière vérifie qu’une mauvaise entrée déclenche bien une erreur, ce qui est la façon de tester votre validation.
Vous ne créez pas ces dossiers à la main. usethis les génère : usethis::use_testthat() une fois par projet, puis usethis::use_test("summarise") crée le fichier de test correspondant (voir la leçon sur le développement de packages pour la structure de package sur laquelle cela repose). Pour exécuter toute la suite, appelez devtools::test() — il exécute chaque fichier test-*.R et rapporte les réussites et les échecs. Tout ici est du testthat standard ; rien n’est encore propre à Shiny, ce qui est précisément le but — votre logique métier ne devrait pas avoir besoin de Shiny pour être testable.
Testez la logique réactive et de module avec testServer()
La logique métier est facile à tester parce que ce ne sont que des fonctions. La logique réactive est la partie difficile — un reactive() n’a de sens qu’à l’intérieur d’une session Shiny en cours d’exécution. La réponse de Shiny est testServer() : il exécute une fonction serveur dans une session sans navigateur où vous définissez les entrées et lisez les réactifs directement, sans navigateur et sans application en cours d’exécution.
Prenez un petit serveur avec un réactif qui additionne deux entrées :
library(shiny)
server <- function(input, output, session) {
total <- reactive(input$a + input$b)
output$sum <- renderText(total())
}Pour le tester, enveloppez-le dans testServer(). À l’intérieur du bloc, session$setInputs() simule l’utilisateur qui définit les entrées, et vous lisez le réactif en l’appelant — total() — exactement comme le fait le serveur :
testServer(server, {
session$setInputs(a = 2, b = 5)
expect_equal(total(), 7)
})Définissez a = 2 et b = 5, et total() renvoie 7. Aucune interface n’a été rendue, aucun navigateur ouvert — testServer() a piloté le graphe réactif dans le processus et vous a remis la valeur à vérifier. C’est toute l’idée : parce qu’une fonction serveur est une fonction ordinaire de (input, output, session), sa logique réactive est directement testable.
Le même outil teste un serveur de module, et c’est là qu’il prouve sa valeur. Passez la fonction serveur du module au lieu d’un serveur de premier niveau :
testServer(counterServer, {
session$setInputs(add = 1)
session$setInputs(add = 1)
expect_equal(count(), 2)
})C’est le counterServer de la leçon sur les modules — deux clics simulés, et le réactif count qu’il renvoie affiche 2. Un bloc copié-collé enfoui dans une fonction server monolithique ne peut pas être testé ainsi ; un module le peut, ce qui est une raison de plus de modulariser. L’article officiel sur testServer fournit la référence complète.
Voyez le graphe réactif avec reactlog
Quand une application se comporte mal côté réactivité — une sortie qui ne se met pas à jour, ou un réactif qui se déclenche à chaque frappe — vous devez voir le graphe, pas le deviner. reactlog enregistre chaque événement réactif et dessine le graphe de dépendances : chaque entrée, réactif et sortie comme un nœud, les arêtes montrant ce qui dépend de quoi, rejouable pas à pas.
Activez-le avant de lancer l’application :
library(shiny)
reactlog::reactlog_enable() # or: options(shiny.reactlog = TRUE)
runApp("path/to/app")Utilisez l’application un moment — cliquez sur des éléments, changez des entrées — puis ouvrez le graphe. Deux façons : appuyez sur Ctrl+F3 (Cmd+F3 sur Mac) dans le navigateur pendant que l’application tourne, ou arrêtez l’application et appelez shiny::reactlogShow() dans la console. L’une ou l’autre ouvre un diagramme interactif où vous pouvez parcourir pas à pas ce qui s’est réexécuté et dans quel ordre.
Lisez-le pour répondre aux questions qui vous bloquent autrement : Pourquoi cette sortie s’est-elle réexécutée ? — remontez ses arêtes entrantes jusqu’à l’entrée qui a changé. Pourquoi cela ne se met-il pas à jour ? — une arête manquante signifie que la dépendance que vous supposiez n’existe pas. Pourquoi l’application est-elle lente ? — un réactif coûteux avec trop de dépendants s’illumine chaque fois que quoi que ce soit en amont change. Le graphe rend visible un problème invisible ; c’est l’outil de débogage réactif le plus utile que possède Shiny.
Parcourez un réactif pas à pas avec browser()
reactlog vous montre la forme du problème ; browser() vous permet de vous arrêter à l’intérieur de celui-ci. Placez browser() dans n’importe quel réactif, observateur ou fonction de rendu : quand l’exécution l’atteint, l’application se met en pause et vous dépose dans une console R interactive — à cet endroit précis, dans cette portée précise. Vous pouvez inspecter input$x, afficher les valeurs intermédiaires et avancer pas à pas :
server <- function(input, output, session) {
filtered <- reactive({
browser() # pause here; inspect input$cyl and data
mtcars[mtcars$cyl == input$cyl, ]
})
output$table <- renderTable(filtered())
}Quand le réactif s’exécute, vous atterrissez dans la console avec input$cyl et tout le reste dans la portée — tapez input$cyl pour voir sa valeur, n pour passer à la ligne suivante, c pour continuer. C’est le moyen le plus rapide de répondre à « quelle est cette valeur en ce moment même ? »
Pour les erreurs en particulier, définissez options(shiny.error = browser) en haut de votre application : au lieu d’afficher une trace d’appels, Shiny ouvre le débogueur à l’endroit où l’erreur s’est produite, de sorte que vous vous tenez exactement là où ça a cassé. Et pour un traçage léger sans mise en pause, parsemez des appels cat(), print() ou message() pour consigner des valeurs dans la console R pendant que l’application tourne — plus rudimentaire que browser(), mais parfois suffisant. (Pensez à retirer browser() et les appels cat() égarés avant de livrer.)
Transformez les traces d’appels en messages clairs
Une suite de tests attrape vos bugs ; une gestion élégante des erreurs attrape ceux de l’utilisateur. Quand une entrée est vide ou qu’un filtre ne correspond à aucune ligne, le comportement par défaut est une vilaine erreur rouge — mauvaise pour l’utilisateur et alarmante pour vous. Shiny vous donne trois outils pour gérer cela proprement.
req() — abréviation de « require » — arrête silencieusement un réactif jusqu’à ce que ses entrées existent, de sorte que rien ne provoque d’erreur avant que l’utilisateur n’ait rien choisi :
output$plot <- renderPlot({
req(input$file) # do nothing until a file is chosen — no red error
plot(read_data(input$file))
})validate() avec need() remplace une trace d’appels par un message clair dans la sortie elle-même :
output$table <- renderTable({
data <- filtered_data()
validate(need(nrow(data) > 0, "No rows match your filter."))
data
})Désormais, un résultat vide affiche « No rows match your filter. » là où la table apparaîtrait, au lieu d’un plantage. Pour tout ce qui pourrait échouer par ailleurs — un fichier qui refuse d’être analysé, une API en panne — enveloppez-le dans tryCatch() et renvoyez un repli sensé. Ensemble, req(), validate() et tryCatch() sont ce qui sépare une application qui casse devant l’utilisateur d’une application qui s’explique. Le chapitre sur le débogage de Mastering Shiny approfondit ce flux de travail.
Au-delà de la boîte à outils du quotidien
Deux choses se situent juste à l’extérieur de cette leçon, et il vaut la peine de savoir où elles se trouvent :
- Les tests de bout en bout — piloter l’interface réelle dans un navigateur sans affichage pour tester des parcours utilisateur complets — sont l’affaire de shinytest2. Faites-y appel quand vous devez vérifier ce sur quoi l’utilisateur clique réellement.
- Les tests de charge — mesurer comment l’application tient sous de nombreux utilisateurs simultanés — sont ce à quoi sert
shinyloadtest. C’est une question de passage à l’échelle, pas de correction.
Et une fois que vous avez une suite de tests, câblez-la dans l’intégration continue pour qu’elle s’exécute à chaque push : la leçon sur le contrôle de version explique comment mettre cela en place. devtools::test() dans une tâche de CI signifie qu’une régression fait échouer le build avant même qu’il n’atteigne main.
Problèmes fréquents
Votre test testServer() échoue parce que vous n’avez jamais défini les entrées. Un réactif qui lit input$a a besoin que input$a existe ; si vous appelez total() avant session$setInputs(a = ..., b = ...), vous obtenez une erreur au sujet d’une entrée manquante. Définissez d’abord chaque entrée dont dépend le réactif, puis lisez le réactif. L’ordre à l’intérieur du bloc testServer() compte — les entrées avant les assertions.
Une « boucle réactive infinie » — l’application se fige ou la console déborde. Cela se produit quand un réactif écrit dans une entrée (ou une valeur réactive) qu’il lit aussi, de sorte que chaque exécution en déclenche une autre. reactlog le rend évident : vous verrez un cycle dans le graphe. La solution est de casser la boucle — enveloppez la lecture dans isolate() pour qu’elle ne crée pas de dépendance, ou repensez la conception pour que les données circulent dans un seul sens. Un réactif doit dépendre de ses entrées, jamais les alimenter.
Vous avez livré une application avec browser() encore dedans. Un browser() égaré met l’application en pause pour chaque utilisateur qui emprunte ce chemin de code — en production, cela ressemble à un blocage. Retirez chaque browser() (et les appels de débogage cat()/print()) avant de déployer. Une recherche rapide de browser( dans tout le projet avant de livrer évite l’embarras.
Questions fréquentes
Travaillez par niveaux. Testez unitairement vos fonctions de logique métier avec testthat — déplacez les fonctions pures dans R/, écrivez des blocs test_that() avec expect_equal(), et exécutez-les avec devtools::test(). Testez la logique réactive et de module avec testServer(), qui pilote les entrées et les réactifs d’une fonction serveur sans navigateur. Pour des tests complets de parcours utilisateur à travers un vrai navigateur, utilisez shinytest2. Placez l’essentiel de votre couverture dans les niveaux rapides — unitaire et testServer().
testServer() est l’outil intégré de Shiny pour tester la logique serveur sans navigateur. Vous lui passez une fonction serveur (un serveur de premier niveau ou un serveur de module) et un bloc de code de test ; à l’intérieur, session$setInputs() simule les entrées de l’utilisateur et vous lisez les réactifs en les appelant — testServer(server, { session$setInputs(a = 2, b = 5); expect_equal(total(), 7) }). Il exécute le graphe réactif dans le processus et renvoie de vraies valeurs sur lesquelles vous pouvez faire des assertions, ce qui rend la logique réactive testable unitairement.
Deux outils. Pour voir le graphe réactif, activez reactlog avec reactlog::reactlog_enable() avant de lancer l’application, puis appuyez sur Ctrl+F3 (Cmd+F3 sur Mac) dans le navigateur — ou appelez shiny::reactlogShow() ensuite — pour ouvrir une carte interactive de chaque réactif et de ses dépendances, afin de voir pourquoi quelque chose s’est réexécuté ou non. Pour entrer à l’intérieur d’un réactif, placez-y browser() pour mettre en pause et inspecter les valeurs, ou définissez options(shiny.error = browser) pour ouvrir le débogueur dès qu’une erreur se déclenche.
Un test unitaire vérifie une fonction pure de façon isolée avec testthat — rapide, fiable, et c’est là que devraient résider la plupart de vos tests. Un test d’intégration (ou réactif) vérifie que les composants fonctionnent ensemble : testServer() vérifie qu’une chaîne réactive ou un serveur de module produit la bonne sortie, et les tests de bout en bout avec shinytest2 pilotent l’application entière dans un navigateur pour vérifier des parcours utilisateur complets. Suivez la pyramide des tests — de nombreux tests unitaires, quelques tests testServer(), quelques tests de bout en bout lents.
Utilisez req(). Appeler req(input$file) en haut d’un réactif ou d’un rendu l’arrête silencieusement jusqu’à ce que input$file ait une valeur, de sorte que la sortie reste vide au lieu de provoquer une erreur avant que l’utilisateur n’ait rien choisi. Pour un message clair quand un calcul n’a légitimement aucun résultat — un filtre vide, par exemple — utilisez validate(need(nrow(data) > 0, "No rows match your filter.")), qui affiche votre message dans la sortie au lieu d’une trace d’appels. Enveloppez tout le reste susceptible d’échouer dans tryCatch().
Testez vos connaissances
Vous avez une fonction utilitaire et un petit serveur :
# a pure helper you want to trust
mean_hp <- function(data) mean(data$hp)
# a server with one reactive
server <- function(input, output, session) {
doubled <- reactive(input$x * 2)
output$out <- renderText(doubled())
}Écrivez (1) un test testthat vérifiant que mean_hp(mtcars) renvoie la puissance moyenne de mtcars (arrondie à 2 décimales, soit 146.69), et (2) un test testServer() vérifiant que lorsque input$x vaut 4, doubled() renvoie 8.
Un test testthat est un bloc test_that("description", { ... }) avec un expect_equal() à l’intérieur. Pour le réactif, enveloppez le serveur dans testServer(server, { ... }), appelez session$setInputs(x = 4) avant de lire doubled(), puis vérifiez avec expect_equal(doubled(), 8).
library(shiny)
library(testthat)
# 1. Unit-test the pure helper
test_that("mean_hp computes mean horsepower", {
expect_equal(round(mean_hp(mtcars), 2), 146.69)
})
# 2. Test the reactive headless with testServer
test_that("doubled() doubles its input", {
testServer(server, {
session$setInputs(x = 4) # set the input FIRST
expect_equal(doubled(), 8) # then read the reactive
})
})Le test de la fonction utilitaire n’a besoin d’aucun Shiny — c’est une simple vérification de fonction. Le test du réactif utilise testServer() pour exécuter le serveur dans une session sans navigateur : session$setInputs(x = 4) simule l’entrée, et doubled() lit le réactif, qui renvoie 8. Définissez les entrées avant de lire le réactif, sinon il échouera sur une entrée manquante.
Quel outil teste la logique réactive d’une application Shiny sans lancer de navigateur ?
A. shinytest2 — il pilote l’application dans un navigateur sans affichage.
B. testServer() — il exécute les réactifs d’une fonction serveur dans le processus, sans navigateur.
C. reactlog — il visualise le graphe réactif.
D. browser() — il met en pause l’exécution à l’intérieur d’un réactif.
B. testServer() exécute une fonction serveur ou serveur de module dans une session sans navigateur où vous définissez les entrées et lisez les réactifs directement — sans navigateur, sans application en cours d’exécution — c’est donc l’outil pour tester unitairement la logique réactive. A est faux : shinytest2 utilise bel et bien un navigateur (sans affichage) et sert aux tests de bout en bout. C est un visualiseur de débogage, pas un test — il montre le graphe mais ne vérifie rien. D met en pause l’exécution pour une inspection interactive ; c’est une aide au débogage, pas un test.
Conclusion
Les tests et le débogage sont ce qui transforme une application Shiny qui « marche sur ma machine » en une application que vous pouvez modifier sans crainte. Testez unitairement la logique métier avec testthat pour qu’une régression fasse échouer un test au lieu de surprendre un utilisateur. Testez la logique réactive et de module avec testServer(), qui prouve la logique sans navigateur et renvoie de vraies valeurs sur lesquelles faire des assertions — aucun navigateur requis. Quand quelque chose se comporte mal côté réactivité, faites appel à reactlog pour voir le graphe et à browser() pour entrer à l’intérieur. Et gérez les erreurs que vos utilisateurs rencontreront avec req(), validate() et tryCatch(), pour qu’une trace d’appels devienne une phrase sur laquelle ils peuvent agir. Gardez l’essentiel de vos tests dans les niveaux rapides — unitaire et testServer() —, réservez les tests de bout en bout aux parcours qui comptent, et laissez le runtime être le juge.
Leçons connexes
- Appuyez-vous sur les éléments que ceci teste : la leçon Modules Shiny enseigne le motif
moduleServer()quetestServer()teste sans navigateur (c’est là que vit l’exemplecounterServer), et le développement de packages ainsi que l’organisation du code couvrent la structureR/ettests/qu’attendtestthat. Une fois que vous avez une suite, câblez-la dans la CI avec le contrôle de version pour qu’elle s’exécute à chaque push. - Allez plus loin : Applications web Shiny — tout le cursus Shiny. · Science computationnelle et IA — le pilier.
Prouvez que vous savez le faire. Maîtrisez toute la série Architecture Shiny — suivez votre parcours, construisez des projets et obtenez un certificat.
Passez à Pro — Prova illimité sur vos propres données et un certificat vérifiable qui atteste la compétence.
dès 15 $/mois facturé annuellement
✓ Vous êtes Pro — continuez. The runtime is the judge.
Réutilisation
Citation
@online{2026,
author = {},
title = {Tests et débogage Shiny : testthat, testServer et reactlog},
date = {2026-07-16},
url = {https://www.datanovia.com/learn/programming/shiny/architecture/testing-debugging},
langid = {fr}
}