Utilisez Hypothesis pour automatiser la création de votre cas de test
Les tests unitaires sont la norme de facto de l’industrie utilisée dans tous les projets de logiciels professionnels – comme il se doit. Nous en avons discuté plus dans différents articles (introduction des tests unitaires pour Python et railleur) et créé des articles similaires pour C++ (Partie 1 / Partie 2).
Cependant, cela ne signifie pas que les tests unitaires sont parfaits. Un inconvénient est sa nature intrinsèquement axée sur l’exemple : les développeurs écrivent des tests unitaires en fonction de leur compréhension de leur code, en suivant leurs idées de bien/mal, ce qui crée des limites pour la fonction en question. Lorsqu’un test unitaire échoue, nous avons certainement trouvé une erreur dans le code (en supposant que le test était correct), mais lorsqu’il réussit, nous nous posons la question : ce test a-t-il couvert toutes les propriétés pertinentes de la fonction en question ? Couvre-t-il tous les cas d’angle ?
Suite aux réflexions ci-dessus, il semble naturel d’introduire des tests basés sur les propriétés. Ceux-ci proviennent du monde de la programmation fonctionnelle et, comme leur nom l’indique, définissent les propriétés souhaitées d’une fonction. Considérons par exemple une fonction codeur/décodeur. Il semble naturel de s’attendre dec(enc(x)) = x
. Parallèlement à cela, nous introduisons la génération automatisée de cas de test. Ce n’est qu’en combinant cela et les propriétés de test des cas de test que nous obtenons plus de puissance que dans les tests unitaires « communs » (de même, les tests basés sur les propriétés ont permis la génération automatisée de cas de test, car maintenant nous n’avons plus besoin de connaître la réponse exacte pour chaque cas de test).
Dans cet article, nous présenterons les tests basés sur les propriétés pour Python en utilisant le Hypothèse. Il peut être utilisé pour créer automatiquement des cas de test en suivant certaines stratégies personnalisables. Avec cela, nous pouvons écrire des tests significatifs basés sur les propriétés ou faire test fuzz. Toutefois, ce dernier ne fait pas partie du présent article. Mais, Hypothesis offre bien plus, comme la réduction des cas de test et le stockage des mauvais exemples précédents dans une base de données – que nous aborderons dans cet article.
Notez que les tests basés sur les propriétés ne doivent pas remplacer les tests basés sur des exemples, mais sont/peuvent être un complément utile. Nous discuterons des propriétés utiles plus tard, mais nous ne pouvons pas tout tester avec cette méthode. Nous sommes limités par ce que nous pouvons exprimer comme une « propriété ». De plus, nous devons tenir compte du temps supplémentaire nécessaire pour exécuter des tests, car l’hypothèse crée N
cas de test aléatoires et les exécute tous.
Avant d’aller plus loin, commençons par un exemple introductif simple. Pour cela, considérons l’encodeur/exemple mentionné ci-dessus. Nous implémentons un chiffrement simple ressemblant au célèbre Chiffre de César: chaque lettre du texte en clair est remplacée par une autre, qui suit c
caractères plus tard dans l’alphabet que la lettre d’origine — enc(“hello”, 1)
donne ifmmp
. De plus, nous avons ajouté un simple test unitaire paramétré dans le même fichier (à des fins de démonstration uniquement). Voici à quoi cela ressemble :
import pytestdef encode(plaintext: str, key: int) -> str:
return "".join(chr(ord(c) + key) for c in plaintext)
def decode(ciphertext: str, key: int) -> str:
return "".join(chr(ord(c) - key) for c in ciphertext)
@pytest.mark.parametrize(
"plaintext, key, expected_ciphertext",
[("test", 0, "test"), ("hello", 1, "ifmmp")],
)
def test_encode(plaintext: str, key: int, expected_ciphertext: str) -> None:
assert encode(plaintext, key) == expected_ciphertext
Nous pouvons exécuter ce fichier de test via python -m pytest main.py
.
Maintenant, ajoutons un test avec Hypothesis :
import pytest
from hypothesis import given
from hypothesis import strategies as stdef encode(plaintext: str, key: int) -> str:
return "".join(chr(ord(c) + key) for c in plaintext)
def decode(ciphertext: str, key: int) -> str:
return "".join(chr(ord(c) - key) for c in ciphertext)
@pytest.mark.parametrize(
"plaintext, key, expected_ciphertext",
[("test", 0, "test"), ("hello", 1, "ifmmp")],
)
def test_encode(plaintext: str, key: int, expected_ciphertext: str) -> None:
assert encode(plaintext, key) == expected_ciphertext
@given(s=st.text(), k=st.integers(0, 26))
def test_decode_inverts_encode(s: str, k: int) -> None:
assert decode(encode(s, k), k) == s
Comme nous pouvons le voir, nous disons à Hypothesis de générer des textes clairs aléatoires s
ainsi que des clés k
, et tester la propriété d’inversibilité. Nous permettons s
être n’importe quel texte mais restreindre k
à la gamme [0, 26]c’est-à-dire un décalage de type caractère, pour éviter les dépassements excessifs / insuffisants pour le ord
fonction (ce que l’hypothèse indique correctement autrement !)
Lors de l’exécution d’un test, par défaut, Hypothesis créera 100 cas de test aléatoires et les exécutera (notez le commentaire d’introduction sur le temps d’exécution. Si cela est critique pour vous, soyez prudent). Il est intéressant d’entrevoir les éléments internes d’Hypothesis, ce que vous pouvez facilement faire en ajoutant des entrées de test à une liste globale et en l’imprimant plus tard.
Réduction des cas de test
Une autre caractéristique intéressante d’Hypothesis est la réduction des cas de test, qui se fait par rétrécissement. Cela signifie que Hypothesis produira toujours l’exemple de falsification « le plus simple ». Considérez le cas de test artificiel suivant, car il génère une erreur si le int passé est supérieur à 10
:
@given(i=st.integers())
def test_ints(i: int) -> None:
assert i < 10
L’hypothèse sortira 10
comme exemple de falsification – et non aucun des autres nombres arbitrairement grands qu’il a probablement essayés (comment il trouve 10
est défini par la stratégie spécifique suivie pour générer les entrées). Ceci est très utile et permet souvent de comprendre plus facilement ce qui n’a pas fonctionné.
Enregistrement d’exemples de falsification
L’hypothèse ne partira pas toujours de zéro lorsqu’il s’agit de trouver des exemples falsifiants. Au lieu de cela, il conservera une base de données des erreurs précédemment trouvées, gagnera du temps lors de la découverte d’erreurs récurrentes et réduira la possibilité de manquer un exemple de falsification. Puisque nous générons une quantité finie, c’est aussi une possibilité, bien que rare.
Cette base de données se trouve dans le dossier .hypothesis
, sous le répertoire à partir duquel nous exécutons le test. Le supprimer efface la base de données.
Passons en revue quelques exemples supplémentaires qui vous donneront une bonne intuition de ce que l’hypothèse peut faire et comment l’utiliser. Dans cette section, nous nous concentrons sur la définition des tests et des stratégies et nous nous soucions moins du contenu et des propriétés des tests réels. Nous en discuterons dans la section suivante.
Stratégies
L’hypothèse définit un large éventail de « stratégies » que nous pouvons utiliser pour générer des cas de test. Nous avons utilisé certains d’entre eux auparavant (par exemple, st.integers()
).
Les plus basiques sont booleans
, integers
, floats
, text
. Écrivons un test factice vérifiant que les entrées générées correspondent bien à nos attentes :
@given(i=st.integers(), b=st.booleans(), t=st.text(), f=st.floats(-10, 10))
def test_strategies(i: int, b: bool, t: str, f: float) -> None:
assert type(i) == int
assert type(b) == bool
assert type
Télécharger ici