Bases de Pytest

Terminé

Commençons à tester avec Pytest. Comme nous l’avons mentionné dans l’unité précédente, Pytest est hautement configurable et peut gérer des suites de tests complexes, mais il ne nécessite pas beaucoup pour commencer à écrire des tests. En fait, plus un framework vous permet d’écrire des tests, mieux c’est.

À la fin de cette section, vous devriez avoir tout ce dont vous avez besoin pour commencer à écrire vos premiers tests et les exécuter avec Pytest.

Conventions

Avant de nous plonger dans l’écriture de tests, nous devons aborder certaines des conventions de test sur lesquelles Pytest s’appuie.

Il n’existe pas de nombreuses règles difficiles sur les fichiers de test, les répertoires de test ou les dispositions de test générales dans Python. En connaissant ces règles, vous pouvez tirer parti de la découverte et de l’exécution automatiques des tests sans avoir besoin d’une configuration supplémentaire.

Répertoire des tests et fichiers de test

Le répertoire principal pour les tests est le répertoire tests. Vous pouvez placer ce répertoire au niveau racine du projet, mais il n’est pas rare de le voir à côté des modules de code.

Remarque

Dans ce module, nous utiliserons par défaut des tests à la racine d’un projet.

Voyons à quoi ressemble la racine d’un petit projet Python appelé jformat :

.
├── README.md
├── jformat
│   ├── __init__.py
│   └── main.py
├── setup.py
└── tests
    └── test_main.py

Le répertoire tests se trouve à la racine du projet avec un seul fichier de test. Dans le cas présent, le fichier de test s’appelle test_main.py. Cet exemple illustre deux conventions essentielles :

  • Utiliser un répertoire tests pour placer des fichiers de test et des répertoires de test imbriqués.
  • Préfixer les fichiers de test avec test. Le préfixe indique que le fichier contient du code de test.

Attention

Évitez d’utiliser test (forme au singulier) comme nom de répertoire. Le nom test est un module Python, donc créer un répertoire appelé de la même façon le remplacerait. Utilisez toujours tests au pluriel à la place.

Tester des fonctions

Un argument fort pour l’utilisation de Pytest est qu’il vous permet d’écrire des fonctions de test. Comme pour les fichiers de test, les fonction test doivent être préfixées avec test_. Le préfixe test_ garantit que Pytest collecte le test et l’exécute.

Voici à quoi ressemble une fonction de test simple :

def test_main():
    assert "a string value" == "a string value"

Remarque

Si vous connaissez unittest, il peut paraître surprenant de voir l’utilisation de assert dans la fonction de test. Nous abordons les assertions brutes plus en détail plus tard, mais avec Pytest, vous obtenez des rapports d’échec complets avec des assertions brutes.

Classes de test et méthodes de test

Comme pour les fichiers et les fonctions, les classes et les méthodes de test utilisent les conventions suivantes :

  • Les classes de test sont préfixées avec Test
  • Les méthodes de test sont préfixées avec test_

Une différence essentielle avec la bibliothèque unittest de Python est qu’il n’y a pas besoin d’héritage.

L'exemple suivant utilise ces préfixes et d'autres conventions de dénomination Python pour les classes et les méthodes. Il s'agit d'une petite classe de test qui vérifie les noms d'utilisateur dans une application.

class TestUser:

    def test_username(self):
        assert default() == "default username"

Exécuter les tests

Pytest est à la fois un framework de test et un exécuteur de test. L’exécuteur de test est un exécutable dans la ligne de commande qui (à un niveau élevé) peut :

  • Effectuer la collecte des tests en recherchant tous les fichiers de test, classes de test et fonctions de test pour une série de tests.
  • Lancer une série de tests en exécutant tous les tests.
  • Effectuer le suivi des échecs, des erreurs et de la réussite des tests.
  • Fournir des rapports complets à la fin d’une série de tests.

Remarque

Étant donné que Pytest est une bibliothèque externe, elle doit être installée pour l’utiliser.

Avec ces éléments dans un fichier test_main.py, nous pouvons voir comment Pytest se comporte quand il exécute les tests :

# contents of test_main.py file

def test_main():
    assert True

Dans la ligne de commande, dans le chemin où se trouve le fichier test_main.py, nous pouvons exécuter l’exécutable pytest :

 $ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item

test_main.py .                                                       [100%]

============================ 1 passed in 0.00s =============================

En arrière-plan, Pytest collecte l’exemple de test dans le fichier de test sans aucune configuration nécessaire.

L’instruction assert puissante

Jusqu’à présent, nos exemples de test utilisent tous l’appel brut assert. En règle générale, dans Python, l’instruction assert n’est pas utilisée pour les tests, car elle ne dispose pas de rapports appropriés lorsque l’assertion échoue. Pytest, cependant, n’a pas cette limitation. En arrière-plan, Pytest permet à l’instruction d’effectuer des comparaisons enrichies sans forcer l’utilisateur à écrire plus de code ou à configurer quoi que ce soit.

À l’aide de l’instruction simple assert , vous pouvez utiliser les opérateurs de Python ; par exemple, >, , <, !=, >=ou <=. Tous les opérateurs de Python sont valides. Cette fonctionnalité est probablement la plus cruciale de Pytest : vous n’êtes pas obligé d’apprendre la nouvelle syntaxe pour écrire des assertions.

Voyons comment cela se traduit lors de comparaisons courantes avec des objets Python. Dans le cas présent, nous allons passer en revue le rapport d’échec lors de la comparaison de chaînes longues :

================================= FAILURES =================================
____________________________ test_long_strings _____________________________

    def test_long_strings():
        left = "this is a very long strings to be compared with another long string"
        right = "This is a very long string to be compared with another long string"
>       assert left == right
E       AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E         - This is a very long string to be compared with another long string
E         ? ^
E         + this is a very long strings to be compared with another long string
E         ? ^                         +

test_main.py:4: AssertionError

Pytest montre un contexte utile autour de l’échec : une erreur de majuscule/minuscule au début de la chaîne et un caractère supplémentaire dans un mot. Mais au-delà des chaînes, Pytest peut vous aider avec d’autres objets et structures de données. Par exemple, voici comment il se comporte avec les listes :

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E         At index 1 diff: 'wheat' != 'coffee'
E         Full diff:
E         - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E         ?                     ---------
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?           +++++++++

test_main.py:9: AssertionError

Ce rapport identifie que l’index 1 (deuxième élément de la liste) est différent. Non seulement il identifie le numéro d’index, mais il fournit également une représentation de l’échec. En plus des comparaisons des éléments, il peut également signaler si des éléments sont manquants et fournir des informations qui peuvent vous indiquer exactement de quel élément il peut s’agir. Dans le cas suivant c’est "milk" :

________________________________ test_lists ________________________________

    def test_lists():
        left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
        right = ["sugar", "wheat", "salt", "water", "milk"]
>       assert left == right
E       AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E         At index 2 diff: 'coffee' != 'salt'
E         Left contains one more item: 'milk'
E         Full diff:
E         - ['sugar', 'wheat', 'salt', 'water', 'milk']
E         + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E         ?                    ++++++++++

test_main.py:9: AssertionError

Pour finir, voyons comment il se comporte avec les dictionnaires. La comparaison de deux grands dictionnaires peut être impressionnante en cas d’échecs, mais Pytest fait un travail exceptionnel pour fournir le contexte et identifier l’échec :

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E         Omitting 3 identical items, use -vv to show
E         Differing items:
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         {'number': 39} != {'number': 38}
E         Full diff:
E           {
E            'county': 'Frett',...
E
E         ...Full output truncated (12 lines hidden), use '-vv' to show

Dans ce test, il existe deux échecs dans le dictionnaire. L’un est que la valeur "street" est différente, et l’autre est que "number" ne correspond pas.

Pytest détecte précisément ces différences (même s’il s’agit d’un échec dans un seul test). Étant donné que les dictionnaires contiennent de nombreux éléments, Pytest omet les parties identiques et affiche uniquement le contenu pertinent. Voyons ce qui se passe si nous utilisons l’indicateur -vv suggéré pour augmenter la verbosité dans la sortie :

____________________________ test_dictionaries _____________________________

    def test_dictionaries():
        left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
        right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
>       assert left == right
E       AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E         Common items:
E         {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E         Differing items:
E         {'number': 39} != {'number': 38}
E         {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E         Full diff:
E           {
E            'county': 'Frett',
E         -  'number': 38,
E         ?             ^
E         +  'number': 39,
E         ?             ^
E            'state': 'Nevada',
E         -  'street': 'Ferry Lane',
E         ?                    - ^
E         +  'street': 'Ferry Ln.',
E         ?                     ^
E            'zipcode': 30877,
E           }

En exécutant pytest -vv, le rapport augmente la quantité de détails et fournit une comparaison précise. Non seulement ce rapport détecte et montre l’échec, mais il vous permet d’apporter rapidement des modifications pour corriger le problème.