Les tests unitaires en R. Vous savez, ces petites fonctions qui garantissent que votre code fonctionne à la perfection, même lorsque vous partez en vacances ou que vous écrivez de nouveaux modules à 2h du matin.
Mais, soyons honnêtes, quand il s’agit de tester des interactions utilisateur ou des appels à des ressources externes, ça peut devenir un vrai casse-tête.
Que faire quand votre code nécessite une boîte de dialogue pour que l’utilisateur choisisse un fichier, ou une connexion à une API qui prend une éternité à répondre ?
Pas question de tout bloquer juste pour un test !
Dans ces cas-là, il faut ruser. L’idée, ce n’est pas de renoncer à tester, mais de contourner intelligemment ce qui pose problème. Et c’est là qu’une technique bien connue entre en scène : le mocking
.
R et le package testthat
offrent justement des outils pour ça.
Le mocking
, c’est la possibilité de remplacer temporairement une fonction ou une ressource par une version fictive, contrôlée, qui simule exactement le comportement attendu.
Intrigué ? Continuons.
L’Art de Faire Semblant
: Simuler les interactions utilisateur
Table des matières
Imaginez une fonction qui demande à l’utilisateur de choisir un fichier. Si vous deviez tester cette fonction avec une vraie boîte de dialogue, ce serait une véritable perte de temps – sans parler du fait que vous ne pouvez pas automatiser cela facilement dans une CI (intégration continue).
C’est là qu’intervient la magie du mocking
.
Voici un exemple de fonction qui demande à l’utilisateur de choisir un fichier :
select_file <- function() {
file_path <- choose.files() # Cette fonction ouvre une boîte de dialogue
cat("You have chosen the file :", file_path)
}
Pour tester cette fonction, vous n’avez pas envie de faire une sélection à chaque fois.
Grâce à with_mocked_bindings()
du package {testthat}
, vous pouvez facilement remplacer la fonction choose.files
par une version qui retourne un chemin de fichier prédéfini.
Vous évitez l’interaction et le test devient instantané :
test_that("select_file fonctionne sans fenêtre de dialogue", {
with_mocked_bindings(
code = {
expect_output(select_file(), "You have chosen the file : C:/fakedir/fakefile.txt")
},
choose.files = function() {"C:/fakedir/fakefile.txt"}
)
})
Voilà ! Vous testez la fonction comme si l’utilisateur avait déjà choisi un fichier, sans aucune fenêtre de dialogue.
Quand simuler devient essentiel : Tester des ressources externes
Les tests unitaires sont aussi souvent utilisés pour vérifier des fonctions qui dépendent de ressources externes, comme des appels API, des bases de données, ou même des interactions réseau.
Imaginons une fonction qui récupère des données depuis une API.
Faire un vrai appel à l’API à chaque test serait long, coûteux et totalement inutile.
Au lieu de cela, vous pouvez simuler la réponse de l’API. Voici comment vous pouvez faire :
fetch_data <- function() {
response <- httr::GET("https://api.exemple.com/data")
content <- httr::content(response)
return(content)
}
Si vous voulez tester cette fonction sans appeler réellement l’API, vous pouvez simuler la réponse avec with_mocked_bindings
.
Voici un exemple où l’appel à l’API est remplacé par une réponse préfabriquée :
Cette fois, vous testez la fonction sans faire de vrai appel API, mais en contrôlant entièrement ce qui est retourné.
Vous pouvez tester différentes situations (réponse correcte, erreur, timeout) sans aucun risque de dépendre de l’état réel de l’API.
⚠️ On ne se mocke pas de n’importe qui : Les limitations du mocking
Ce qu’on ne peut pas mocker directement, par exemple :
Sys.time()
Sys.getenv()
Essayer de les redéfinir avec with_mocked_bindings()
produit :
Error in `local_mocked_bindings(...)`: Can't find binding for `Sys.time`
Pourquoi ça coince ? Parce que certaines fonctions, comme Sys.time()
ou Sys.getenv()
, sont des fonctions primitives en R.
Elles ne sont pas de simples objets R modifiables à la volée – elles sont intégrées au cœur du langage, donc impossibles à surcharger aussi facilement qu’une fonction utilisateur classique.
🩹 La parade : Encapsuler le comportement
La solution : Créer une fonction intermédiaire dans votre code que vous pouvez, elle, mocker
.
Exemple :
# Dans votre package
get_current_time <- function() { Sys.time()}
say_hello <- function() {
if (format(get_current_time(), "%H") == "12") {
"Time for lunch!"
} else {
"Just a little longer..."
}
}
Et dans vos tests :
test_that("say_hello détecte bien midi", {
with_mocked_bindings(
code = expect_equal(say_hello(), "Time for lunch!"),
get_current_time = function() as.POSIXct("2025-04-25 12:00:00")
)
})
🚪 En résumé
-
with_mocked_bindings()
permet de simuler des fonctions dont le comportement varie en fonction de l’utilisateur, du système ou du temps, de façon élégante ; -
Certaines fonctions ne sont pas mockables directement (comme
Sys.time
,Sys.getenv
) : encapsulez-les dans des fonctions à part ; -
C’est un super levier pour des tests rapides, fiables et non interactifs.