goodpractice
fournit des conseils sur les bonnes pratiques lorsqu’on construit un package R. Le package identifie les zones de codes qui pourraient être améliorées. Les conseils incluent les fonctions et syntaxes à éviter, la structure du package, la complexité du code, le formatage du code.
formatR
et styler
pour reformater du code.
covr
permet d’estimer la couverture du package par des tests ainsi que l’identification des fonctions ou zone de code, peu ou pas couvertes par des tests.
usethis
et en particulier usethis::use_spell_check()
(basé sur spelling
) qui permet d’identifier facilement les fautes de frappes et les noms incohérents dans le code.
cli
permet de faciliter la communication avec l’utilisateur grâce à des symboles faciles à insérer et des barres de progression pratiques.
Lorsqu’on construit un package, il est fortement conseillé d’inclure des tests dans le répertoire tests/ du package, qui ont pour but de vérifier que l’ensemble des fonctions du package ont un comportement approprié, notamment :
tinytest
, une alternative à testthat
Le package tinytest
est un package relativement récent (~2 ans pour les premières versions) visant à proposer une alternative à testthat
. Pour rappel testthat
et donc tinytest
permettent de réaliser facilement un certains nombre de tests unitaires à l’aide de fonctions sous la forme expect_xxx
pour vérifier si le résultat d’une fonction :
expect_equal
)expect_equivalent
)expect_identical
)expect_null
)expect_true
ou expect_false
)expect_message
)expect_warning
)expect_error
)expect_silent
)L’ensemble de ces tests doit permettre de vérifier que la fonction réagit correctement à toutes les possibilités d’appels de la fonction, si les erreurs possibles sont correctement gérées et si le fonctionnement attendu est correct.
Ces différents types de tests étaient déjà posssible avec le package testthat
, mais le développement récent de tinytest
permet une alternative avec quelques avantages en plus :
tinytest
dépend d’un nombre de package plus réduit (2 vs 12), ce qui rend plus simple son utilisation dans le cadre d’une intégration continue.tinytest
ne renvoient pas d’erreur lors de l’échec d’un test, ce qui permet de finir l’évaluation de l’ensemble des tests malgré l’échec d’un test, sans avoir à utiliser testthat::test_that
.tinytest
donnent plus de détail sur l’échec d’un test et sont capables de prendre en compte potentiellement plus de changements dans l’environnement.Exemple de test avec testthat
. Noter l’utilisation de testthat::test_that
pour ne pas bloquer l’éxecution du code.
::test_that("Test A", {
testthat::expect_equal(object = 2+1, expected = 2)
testthat })
## ── Failure (<text>:2:3): Test A ────────────────────────────────────────────────
## 2 + 1 not equal to 2.
## 1/1 mismatches
## [1] 3 - 2 == 1
Exemple de test avec tinytest
.
::expect_equal(current = 2+1, target = 2, info = "Test A") tinytest
## ----- FAILED[data]: <-->
## call| tinytest::expect_equal(current = 2 + 1, target = 2, info = "Test A")
## diff| Expected '2', got '3'
## info| Test A
Il y a 3 résultats possible pour un test avec tinytest
: PASSED, FAILED et SIDEFX.
Les tests FAILED peuvent l’être à cause :
data
, comme dans l’exemple précédent)attr
)excp
: erreur ou warning).Les tests SIDEFX révèlent des effets de bords qui peuvent être causés :
env
)wdir
)file
).tinytest
est relativement récent, mais plusieurs packages le mobilisent déjà, comme ttdo
et packager
.
Le package autotest
est un package récent (version 0.0.2.135 au 25/08) permettant la génération automatique de tests relatifs à un package dans son entier ou à une fonction d’un package.
Comment ça marche ?
For each .Rd file in a package, autotest tests the code given in the example section according to the following general steps:
autotest_types()
.Les types de tests générés par autotest
peuvent être visualisés par la fonction autotest_types()
:
<- autotest::autotest_types()
x_list_types head(x_list_types, n = 8)
type | test_name | fn_name | parameter | parameter_type | operation | content | test |
---|---|---|---|---|---|---|---|
dummy | rect_as_other | NA | NA | rectangular | Convert one rectangular class to another | check for error/warning messages | TRUE |
dummy | rect_compare_dims | NA | NA | rectangular | Convert one rectangular class to another | expect dimensions are same | TRUE |
dummy | rect_compare_col_names | NA | NA | rectangular | Convert one rectangular class to another | expect column names are retained | TRUE |
dummy | rect_compare_col_structure | NA | NA | rectangular | Convert one rectangular class to another | expect all columns retain identical structure | TRUE |
dummy | extend_rect_class | NA | NA | rectangular | Extend existent class with new class | (Should yield same result) | TRUE |
dummy | replace_rect_class | NA | NA | rectangular | Replace class with new class | (Should yield same result) | TRUE |
dummy | vector_to_list_col | NA | NA | vector | Convert vector input to list-columns | (Should yield same result) | TRUE |
dummy | vector_custom_class | NA | NA | vector | Custom class definitions for vector input | (Should yield same result) | TRUE |
Certains tests peuvent être désactivés en utilisant le nom des tests de la colonne x_list_types$test_name
et la fonction autotest_types
.
::autotest_types(notest = "vector_to_list_col") %>%
autotesthead(n = 8)
type | test_name | fn_name | parameter | parameter_type | operation | content | test |
---|---|---|---|---|---|---|---|
dummy | rect_as_other | NA | NA | rectangular | Convert one rectangular class to another | check for error/warning messages | TRUE |
dummy | rect_compare_dims | NA | NA | rectangular | Convert one rectangular class to another | expect dimensions are same | TRUE |
dummy | rect_compare_col_names | NA | NA | rectangular | Convert one rectangular class to another | expect column names are retained | TRUE |
dummy | rect_compare_col_structure | NA | NA | rectangular | Convert one rectangular class to another | expect all columns retain identical structure | TRUE |
dummy | extend_rect_class | NA | NA | rectangular | Extend existent class with new class | (Should yield same result) | TRUE |
dummy | replace_rect_class | NA | NA | rectangular | Replace class with new class | (Should yield same result) | TRUE |
dummy | vector_to_list_col | NA | NA | vector | Convert vector input to list-columns | (Should yield same result) | FALSE |
dummy | vector_custom_class | NA | NA | vector | Custom class definitions for vector input | (Should yield same result) | TRUE |
Les tests générés sont très exigeants sur la documentation des arguments, des objets retournés et de l’adéquation des exemples avec leur description dans la documentation.
error
si le test a déclenché une erreurwarning
si le test a déclenche un warningdiagnostic
si le test a fonctionné mais qu’il a détecté une incohérence dans la documentation (le test peut être dû à une erreur/warning aussi, mais dans ce cas il concerne uniquement la documentation).message
si le test a renvoyé un message.Exemple avec le package aricode
:
D’abord on peut identifier l’ensemble des tests qui sont réalisés sans les faire tourner, sur deux fonctions (pour limiter le nombre de tests ici): AMI()
et entropy()
.
library(aricode)
<- autotest::autotest_package("aricode",functions = c("AMI","entropy"), test = FALSE) x_ari
##
## ── autotesting aricode ──
##
## ✔ [1 / 2]: AMI
## ✔ [2 / 2]: entropy
::select(x_ari, -yaml_hash) %>%
dplyrhead(n = 6)
type | test_name | fn_name | parameter | parameter_type | operation | content | test |
---|---|---|---|---|---|---|---|
dummy | int_as_numeric | AMI | c1 | integer vector | Integer value converted to numeric | (Should yield same result) | TRUE |
dummy | vector_custom_class | AMI | c1 | vector | Custom class definitions for vector input | (Should yield same result) | TRUE |
dummy | vector_to_list_col | AMI | c1 | vector | Convert vector input to list-columns | (Should yield same result) | TRUE |
dummy | vector_custom_class | AMI | c2 | vector | Custom class definitions for vector input | (Should yield same result) | TRUE |
dummy | vector_to_list_col | AMI | c2 | vector | Convert vector input to list-columns | (Should yield same result) | TRUE |
dummy | return_successful | AMI | (return object) | (return object) | Check that function successfully returns an object | NA | TRUE |
Ici, 26 tests sont identifiés (seulement 6 sont montrés ci dessous). Si on fait tourner la même fonction en activant les tests, autotest_package
renvoie uniquement les tests qui ont échoué avec une description plus ou moins précise de la raison.
library(aricode)
<- autotest::autotest_package("aricode",functions = c("AMI","entropy"), test = TRUE) x_ari
## ── autotesting aricode ──
##
## ✔ [1 / 2]: AMI
## ✔ [2 / 2]: entropy
::select(x_ari, -yaml_hash) dplyr
type | test_name | fn_name | parameter | parameter_type | operation | content | test |
---|---|---|---|---|---|---|---|
diagnostic | vector_custom_class | AMI | c2 | vector | Custom class definitions for vector input | Function [AMI] errors on vector columns with different classes when submitted as c2 Error message: c1 and c2 must be vectors or factors but not lists. | TRUE |
diagnostic | int_as_numeric | entropy | c1 | integer vector | Integer value converted to numeric | Function [entropy] returns different values when parameter c1] only demontrated or documented as int-valued is submitted as double. | TRUE |
diagnostic | vector_custom_class | entropy | c2 | vector | Custom class definitions for vector input | Function [entropy] errors on vector columns with different classes when submitted as c2 Error message: c1 and c2 must be vectors or factors but not lists. | TRUE |
Sur les deux fonctions testées, il n’y a eu aucune erreurs ni warnings, mais deux problèmes de type diagnostic
:
Les fonctions n’excluent pas la possibilité que c2 soit une liste, mais elles ne le permettent pas pour autant. La documentation devrait mentionner l’impossibilité d’utiliser des listes pour l’argument c2
.
La documentation montre juste des integer dans les exemples, mais la fonction permet d’utiliser des double et ce avec des résultats différents (d’après le test).
autotest
ne permet pas pour l’instant de générer automatiquement des fichiers de tests pour un package, mais peut être mobilisé comme ensemble de test à effectuer sur un package. Une série de fonction expect_xxx
permet ainsi de gérer la sortie de la fonction autotest_package
afin d’échouer selon la présence d’erreur, de warnings dans la colonne type
. Les diagnostic
ne sont ici pas pris en compte. Pour vérifier leur absences il faudrait utiliser une estimation du nombre de ligne (voir plus bas). L’inconvénient étant un traçage probablement limité de l’origine des erreurs. L’idée de générer automatiquement les tests individuels est cependant un objectif probable du package.
::expect_equal(
tinytestnrow(autotest::autotest_package("aricode",functions = c("AMI","entropy"), test = TRUE)),
0
)
## ── autotesting aricode ──
##
## ✔ [1 / 2]: AMI
## ✔ [2 / 2]: entropy
## ----- FAILED[data]: <-->
## call| tinytest::expect_equal(nrow(autotest::autotest_package("aricode",
## call| functions = c("AMI", "entropy"), test = TRUE)), 0)
## diff| Mean absolute difference: 3
En résumé, autotest
permet de mettre en place automatiquement une quantité importante de tests, basés sur la rigueur de la documentation et son adéquation avec le fonctionnement des différentes fonctions. Le niveau d’exigence est cependant très élevé et peut être parfois un peu trop fort. Le package ne s’utilise pas encore de manière très stable et fluide pour des packages plus complexes, avec des dépendances nombreuses ou des fonctions avec des classes de paramètres plus compliquées. La fonction autotest_package
peut ainsi renvoyer des erreurs avant d’arriver au moment des tests sans être très explicite sur le problème rencontré.