parametrize Pytest

Effectué

La fonctionnalité de paramétrisation dans pytest peut sembler initialement complexe, mais son objectif est simple une fois que vous comprenez le problème qu’il résout. Essentiellement, paramétrer vous permet d’exécuter la même fonction de test avec différentes entrées efficacement, ce qui facilite l’exécution d’assertions détaillées et variées avec moins de code.

Lorsque vous appelez parametrize, le premier argument est une chaîne contenant un ou plusieurs noms d'arguments, par exemple "test\_input_". Le deuxième argument contient une liste de valeurs d’argument, par exemple ["27", "6+9", "0", "O"]. Les quatre derniers arguments ont des valeurs par défaut et sont facultatifs.

Vous trouverez la référence de l’API pytest de langue anglaise pour paramétrer ici : pytest. Metafunc.parametrize.

Quand utiliser paramétrer

Deux scénarios courants dans lesquels vous souhaiterez peut-être utiliser la paramétrisation sont les suivants :

  • lors du test des boucles ;
  • Lorsque plusieurs tests affirment le même comportement

Examinons d’abord chaque exemple sans utiliser parametrize, puis avec lui pour montrer comment il peut améliorer nos tests.

Boucles For

Voici un exemple de fonction de test avec une for boucle :

def test_string_is_digit():
    items = ["1", "10", "33"]
    for item in items:
        assert item.isdigit()

Ce test est problématique, car s’il échoue, il peut entraîner plusieurs problèmes, notamment :

  • Rapports de test ambigus : Le rapport de test ne précise pas si un seul élément a échoué ou s’il existe plusieurs échecs.
  • Vue de test unique : Tous les éléments sont considérés comme un test unique, ce qui masque les performances des éléments individuels.
  • Correctifs incertains : Si un échec est corrigé, il n’existe aucun moyen de savoir si tous les problèmes sont résolus sans réexécuter l’intégralité du test.

Nous allons modifier le test pour inclure spécifiquement deux éléments qui sont censés échouer :

def test_string_is_digit():
    items = ["No", "1", "10", "33", "Yes"]
    for item in items:
        assert item.isdigit()

L’exécution du test n’affiche qu’un seul échec, même s’il existe deux éléments non valides dans cette liste :

$ pytest test_items.py
=================================== FAILURES ===================================
_____________________________ test_string_is_digit _____________________________
test_items.py:4: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x103fa1df0>()
E    +    where <built-in method isdigit of str object at 0x103fa1df0> = 'No'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit - AssertionError: assert False
============================== 1 failed in 0.01s ===============================

Il s’agit d’un excellent cas d’usage pour paramétrer. Avant de voir comment mettre à jour le test, examinons une autre situation courante qui n'implique pas de boucles for.

Tests qui affirment le même comportement

Un groupe de tests qui font la même assertion est également un bon candidat pour paramétrer. Si le test précédent a été réécrit avec un test pour chaque élément, il permettrait de mieux signaler les défaillances, mais il serait répétitif :

def test_is_digit_1():
    assert "1".isdigit()

def test_is_digit_10():
    assert "10".isdigit()

def test_is_digit_33():
    assert "33".isdigit()

Ces tests sont mieux en ce sens qu’un échec peut être facilement associé à une seule entrée. Et bien qu’il puisse sembler inhabituel d’avoir plusieurs tests similaires, il est courant de voir des suites de tests de production qui essayent d’être granulaires.

Bien que les tests seraient meilleurs parce qu’ils peuvent signaler exactement ce qui échoue (ou réussit), ils sont également accompagnés des problèmes suivants :

  • Le code est répétitif, ce qui crée une charge de maintenance
  • Il existe un risque de fautes de frappe et d’erreurs lors de la mise à jour des tests
  • Étant donné qu’ils sont répétitifs, les ingénieurs peuvent ne pas inclure tous les cas d’usage et les entrées

Comment utiliser le paramétrage

Maintenant que vous connaissez certains cas d’usage pour paramétrer, nous allons mettre à jour le test qui a utilisé une for boucle qui inclut des éléments défaillants.

Pour utiliser parametrize, vous devez importer pytest en tant que bibliothèque, puis l’utiliser comme décorateur dans la fonction. Voici le test mis à jour :

import pytest

@pytest.mark.parametrize("item", ["No", "1", "10", "33", "Yes"])
def test_string_is_digit(item):
    assert item.isdigit()

Avant d’exécuter les tests, passons en revue les modifications.

Le pytest.mark.parametrize() décorateur définit deux arguments. Le premier argument est une chaîne appelée "item". Cette chaîne est utilisée comme argument nommé pour la fonction de test que vous voyez dans la ligne suivante dans la définition de la fonction de test. Le deuxième argument est la liste des valeurs de test.

Enrichissement des rapports d'erreurs

En arrière-plan, pytest considère chaque élément de cette liste comme un test distinct. Cela signifie que les succès et les échecs des tests sont signalés séparément. Voyons ce qui se passe lors de l’exécution du test avec pytest:

$ pytest test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py F...F                                                      [100%]

=================================== FAILURES ===================================
___________________________ test_string_is_digit[No] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45e30>()
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
__________________________ test_string_is_digit[Yes] ___________________________
test_items.py:5: in test_string_is_digit
    assert item.isdigit()
E   AssertionError: assert False
E    +  where False = <built-in method isdigit of str object at 0x102d45df0>()
E    +    where <built-in method isdigit of str object at 0x102d45df0> = 'Yes'.isdigit
=========================== short test summary info ============================
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False
FAILED test_items.py::test_string_is_digit[Yes] - AssertionError: assert False
========================= 2 failed, 3 passed in 0.07s ==========================

Il existe quelques éléments notables dans le rapport de test. Tout d’abord nous voyons qu’à partir d’un seul test, pytest signale cinq tests au total : trois réussites et deux échecs. Les échecs sont signalés séparément, en précisant l’entrée qui a échoué.

$ pytest test_items.py
___________________________ test_string_is_digit[No] ___________________________
[...]
E    +    where <built-in method isdigit of str object at 0x102d45e30> = 'No'.isdigit
[...]
FAILED test_items.py::test_string_is_digit[No] - AssertionError: assert False

Il est difficile de rater la valeur qui a provoqué l’échec avec autant d’endroits où elle est signalée.

Utiliser l’indicateur de sortie de verbe

Lorsque les tests sont exécutés en ligne de commande, le rapport de test est minimal lorsque les tests réussissent. Voici à quoi ressemblerait le test après une mise à jour pour corriger les défaillances :

@pytest.mark.parametrize("item", ["0", "1", "10", "33", "9"])
def test_string_is_digit(item):
    assert item.isdigit()

Et l’exécution des tests produit une sortie minimale :

$ pytest test_items.py 
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private
collected 5 items

test_items.py .....                                                      [100%]

============================== 5 passed in 0.01s ===============================

Augmenter le niveau de verbosité montre les valeurs que pytest exécute pour chaque test lors de l’utilisation de parametrize :

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 5 items

test_items.py::test_string_is_digit[0] PASSED                            [ 20%]
test_items.py::test_string_is_digit[1] PASSED                            [ 40%]
test_items.py::test_string_is_digit[10] PASSED                           [ 60%]
test_items.py::test_string_is_digit[33] PASSED                           [ 80%]
test_items.py::test_string_is_digit[9] PASSED                            [100%]

============================== 5 passed in 0.01s ===============================

Comment utiliser plusieurs noms d’arguments

Les exemples que nous avons vus jusqu’à présent n’ont qu’un nom d’argument dans le premier argument. Nous avons utilisé "item" , mais vous pouvez inclure plusieurs noms d’arguments dans la chaîne qui spécifie le premier argument séparé par des virgules.

L’un des cas d’usage pour l’utilisation de plusieurs noms d’arguments consiste à passer un ensemble de valeurs attendues à tester par rapport à votre valeur d’entrée. Dans votre deuxième argument, chaque élément de votre jeu doit avoir une quantité de valeurs égale au nombre de noms d’entrée. Par exemple, si vos noms d’entrée sont "test\_input, expected\_value", votre deuxième argument peut ressembler à ceci : [(« 3+5 », 8), (« 3*4 », 12)]

Ce test vérifie si un objet a un attribut à l’aide de la fonction Python hasattr() . Elle retourne une valeur booléenne selon que l’objet possède l’attribut associé.

>>> hasattr(dict(), "keys")
True
>>> hasattr("string", "append")
False

Étant donné qu’il hasattr() faut deux arguments, nous pouvons utiliser paramétrer de la manière suivante :

@pytest.mark.parametrize("item, attribute", [("", "format"), (list(), "append")])
def test_attributes(item, attribute):
    assert hasattr(item, attribute)

Le décorateur parametrize utilise toujours une seule chaîne pour le premier argument, mais avec deux noms d’arguments séparés par une virgule, qui deviennent des arguments pour la fonction de test. Dans ce cas, c’est item et attribute.

Voici une liste de deux paires d’éléments. Chacune de ces paires représente un item et un attribute à tester.

Quand pytest ne peut pas générer une représentation sous forme de chaîne des objets transmis, il en crée une. Vous pouvez le voir lors de l’exécution du test :

$ pytest -v test_items.py
============================= test session starts ==============================
Python 3.9.6, pytest-6.2.5, py-1.11.0, pluggy-1.0.0 
rootdir: /private
collected 2 items

test_items.py::test_attributes[-format] PASSED                           [ 50%]
test_items.py::test_attributes[item1-append] PASSED                      [100%]

============================== 2 passed in 0.01s ===============================