Test Python avec pytest. Utilisation de pytest avec d'autres outils, CHAPITRE 7

Retour


En règle générale, pytest n'est pas utilisé indépendamment, mais dans un environnement de test avec d'autres outils. Ce chapitre traite d'autres outils qui sont souvent utilisés en conjonction avec pytest pour des tests efficaces et efficients. Bien qu'il ne s'agisse nullement d'une liste exhaustive, les outils abordés ici vous donneront une idée du goût du pouvoir de mélanger pytest avec d'autres outils.



Les exemples de ce livre sont écrits en utilisant Python 3.6 et pytest 3.2. pytest 3.2 prend en charge Python 2.6, 2.7 et Python 3.3+.


Le code source du projet Tâches, ainsi que pour tous les tests présentés dans ce livre, est disponible sur le lien sur la page Web du livre à pragprog.com . Vous n'avez pas besoin de télécharger le code source pour comprendre le code de test; le code de test est présenté sous une forme pratique dans les exemples. Mais pour suivre les tâches du projet ou adapter des exemples de test pour tester votre propre projet (vos mains ne sont pas liées!), Vous devez vous rendre sur la page Web du livre et télécharger le travail. Là, sur la page Web du livre, il y a un lien pour les messages d' erreur et un forum de discussion .

Sous le spoiler se trouve une liste d'articles de cette série.



pdb: débogage des échecs de test


Le module pdb est un débogueur Python dans la bibliothèque standard. Vous utilisez --pdb pour que pytest démarre une session de débogage au point d'échec. Regardons pdb en action dans le cadre du projet Tâches.


Dans «Paramétrage du luminaire» à la page 64, nous avons laissé le projet Tâches avec quelques erreurs:


 $ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds 

Avant de voir comment pdb peut nous aider à déboguer ce test, examinons les options de pytest disponibles pour accélérer le débogage des erreurs de test, que nous avons abordées dans la section «Utilisation des options» à la page 9:


  • --tb=[auto/long/short/line/native/no] : contrôle le style de trace.
  • -v / --verbose : affiche tous les noms de test qui ont réussi ou échoué.
  • -l / --showlocals : affiche les variables locales à côté de la trace de pile.
  • -lf / --last-failed : exécute uniquement les tests qui échouent.
  • -x / --exitfirst : -x / --exitfirst la session de test lors du premier échec.
  • --pdb : démarre une session de débogage interactive au point d'échec.



Installation de MongoDB




Comme mentionné au chapitre 3, «Appareils Pytest», page 49, l'installation de MongoDB et pymongo est requise pour exécuter les tests MongoDB.


J'ai testé la version de Community Server trouvée sur https://www.mongodb.com/download-center . pymongo s'installe avec pip : pip install pymongo . Cependant, c'est le dernier exemple dans un livre qui utilise MongoDB. Pour essayer le débogueur sans utiliser MongoDB, vous pouvez exécuter les commandes pytest à partir du code/ch2/ , car ce répertoire contient également plusieurs tests ayant échoué.




Nous venons d' code/ch3/c les tests à partir du code/ch3/c pour nous assurer que certains d'entre eux ne fonctionnent pas. Nous n'avons pas vu de tracebacks ou de noms de test car --tb=no désactive le traçage et nous n'avions pas --verbose activé. Répétons les erreurs (pas plus de trois) avec le texte détaillé:


 $ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ==================== 

Nous savons maintenant quels tests ont échoué. Examinons un seul d'entre eux, en utilisant -x , en --tb=no le traçage, en n'utilisant pas --tb=no et en montrant les variables locales avec -l :


 $ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============ 

Très souvent, cela suffit pour comprendre pourquoi le test a échoué. Dans ce cas particulier, il est assez clair que task_id pas un entier - c'est une instance d'ObjectId. ObjectId est le type utilisé par MongoDB pour les identificateurs d'objet dans la base de données. Mon intention avec la couche tasksdb_pymongo.py était de cacher certains détails de l'implémentation de MongoDB du reste du système. Il est clair que dans ce cas cela n'a pas fonctionné.


Cependant, nous voulons voir comment utiliser pdb avec pytest, alors imaginons pourquoi on ne sait pas pourquoi ce test a échoué. Nous pouvons faire en sorte que pytest démarre une session de débogage et nous démarre directement au point d'échec en utilisant --pdb :


 $ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb) 

Maintenant que nous sommes à l'invite (Pdb), nous avons accès à toutes les fonctionnalités de débogage interactif de pdb. Lors de la visualisation des plantages, j'utilise régulièrement ces commandes:


  • p/print expr : imprime la valeur de exp.
  • pp expr : Pretty affiche la valeur de expr.
  • l/list : l/list le point de défaillance et cinq lignes de code au-dessus et en dessous.
  • l/list begin,end : énumère les numéros de ligne spécifiques.
  • a/args : imprime les arguments de la fonction actuelle avec leurs valeurs.
  • u/up : déplace un niveau vers le haut du chemin de la pile.
  • d/down : descend d'un niveau dans la trace de la pile.
  • q/quit : termine une session de débogage.

D'autres commandes de navigation, telles que step et next, ne sont pas très utiles, car nous sommes assis juste dans l'instruction assert. Vous pouvez également simplement entrer des noms de variables et obtenir des valeurs.


Vous pouvez utiliser p/print expr même manière que l' -l/--showlocals pour afficher les valeurs dans une fonction:


 (Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb) 

Vous pouvez maintenant quitter le débogueur et poursuivre les tests.


 (Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds =========== 

Si nous n'utilisions pas - , pytest ouvrirait à nouveau Pdb lors du prochain test. Plus d'informations sur l'utilisation du module pdb sont disponibles dans la documentation Python .


Coverage.py: Déterminer la quantité de code de test


La couverture du code est un indicateur du pourcentage de code testé testé par un ensemble de tests. Lorsque vous exécutez des tests pour le projet Tâches, certaines fonctions Tâches sont exécutées avec chaque test, mais pas avec tous.


Les outils de couverture de code sont parfaits pour vous permettre de savoir quelles parties du système sont complètement ignorées par les tests.


Coverage.py est l'outil de couverture Python préféré qui mesure la couverture de code.


Vous l'utiliserez pour vérifier le code du projet Tâches avec pytest.


Pour utiliser coverage.py vous devez l'installer. Cela pytest-cov pas de mal d'installer un plugin appelé pytest-cov , qui vous permet d'appeler coverage.py depuis pytest avec quelques options pytest supplémentaires. Puisque la coverage est l'une des dépendances de pytest-cov , installez simplement pytest-cov et cela prendra la coverage.py :


 $ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1 

Exécutons le rapport de couverture pour la deuxième version de tâche. Si la première version du projet Tâches est toujours installée, désinstallez-la et installez la version 2:


 $ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ... 

Maintenant que la prochaine version des tâches est installée, vous pouvez exécuter le rapport de couverture de base:


 $ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds =================== 

Étant donné que le répertoire actuel est tasks_proj_v2 et que le code source sous test est dans src, l'ajout de l'option --cov=src génère un rapport de couverture pour ce répertoire uniquement.


Comme vous pouvez le voir, certains fichiers ont une couverture assez faible et même 0%. Ce sont des rappels utiles: tasksdb_pymongo.py 0% car nous avons désactivé le test de MongoDB dans cette version. Certains d'entre eux sont assez faibles. Le projet devra certainement fournir des tests pour tous ces domaines avant d'être prêt pour les heures de grande écoute.


Je pense que plusieurs fichiers ont un pourcentage de couverture plus élevé: api.py et tasksdb_tinydb.py . Jetons un coup d'œil à tasksdb_tinydb.py et voyons ce qui manque. Je pense que la meilleure façon de le faire est d'utiliser des rapports HTML.


Si vous exécutez à nouveau coverage.py avec l' --cov-report=html , un --cov-report=html sera généré:


 $ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds =================== 

Vous pouvez ensuite ouvrir htmlcov/index.html dans un navigateur qui affiche la sortie sur l'écran suivant:



Cliquer sur tasksdb_tinydb.py affichera un rapport pour un fichier. Le pourcentage de lignes couvertes est affiché en haut du rapport, plus le nombre de lignes couvertes et combien ne le sont pas, comme indiqué sur l'écran suivant:



En faisant défiler vers le bas, vous pouvez voir les lignes manquantes, comme indiqué dans l'écran suivant:



Même si cet écran n'est pas une page complète pour ce fichier, cela suffit pour nous dire que:


  1. Nous ne testons pas list_tasks() avec l'ensemble propriétaire.
  2. Nous ne testons pas update() ou delete() .
  3. Peut-être que nous ne testons pas à fond unique_id() .

Super. Nous pouvons les inclure dans notre liste de tests TO-DO avec les tests du système de configuration.


Bien que les outils de couverture de code soient extrêmement utiles, la recherche d'une couverture à 100% peut être dangereuse. Lorsque vous voyez du code qui n'est pas testé, cela peut signifier qu'un test est nécessaire. Mais cela peut également signifier que certaines fonctions du système ne sont pas nécessaires et peuvent être supprimées. Comme tous les outils de développement logiciel, l'analyse de la couverture du code ne remplace pas la réflexion.


Voir la coverage.py et pytest-cov plus de détails.


maquette: substitution de pièces du système


Le package simulé est utilisé pour remplacer des parties du système afin d'isoler des parties du code de test du reste du système. Mock - les objets sont parfois appelés tests doubles, espions, contrefaçons ou talons.


Entre votre propre appareil pytest monkeypatch (décrit dans Utilisation de monkeypatch à la page 85) et la simulation, vous devriez avoir toutes les fonctionnalités de test double nécessaires.


Attention! Moqueur et très bizarre
Si c'est la première fois que vous rencontrez des jumeaux d'essai comme des simulacres, des talons et des espions, préparez-vous! Ce sera très étrange, très rapide, drôle, bien que très impressionnant.

Le package mock est livré avec la bibliothèque Python standard, comme unittest.mock depuis Python 3.3. Dans les versions antérieures, il est disponible sous la forme d'un package distinct installé via PyPI. Cela signifie que vous pouvez utiliser la version fictive PyPI de Python 2.6 vers la dernière version Python et obtenir les mêmes fonctionnalités que la dernière version mock Python. Cependant, pour une utilisation avec pytest, un plugin appelé pytest-mock a certaines fonctionnalités qui en font mon interface préférée pour le système de simulation.


Pour le projet Tâches, nous utiliserons mock pour nous aider à tester l'interface de ligne de commande. Dans Coverage.py: en déterminant la quantité de code testée, à la page 129, vous avez vu que notre fichier cli.py n'a pas été testé du tout. Nous allons commencer à le réparer maintenant. Mais parlons d'abord de stratégie.


La première solution du projet Tâches a api.py à effectuer la plupart des tests de fonctionnalité via api.py Par conséquent, une solution raisonnable est que les tests de ligne de commande ne doivent pas être des tests fonctionnels complets. Nous pouvons être sûrs que le système fonctionnera via la CLI si nous obtenons un niveau API mouillé lors des tests CLI. C'est aussi une solution pratique qui nous permet de regarder moki dans cette section.


L'implémentation des tâches CLI utilise un package d'interface de ligne de commande Click tiers. Il existe de nombreuses alternatives pour implémenter l'interface de ligne de commande, y compris un module intégré à Python argparse . L'une des raisons pour lesquelles j'ai choisi Click est qu'il comprend un moteur de test qui nous aide à tester les applications Click. Cependant, le code dans cli.py , bien que nous espérons qu'il est typique des applications Click, n'est pas évident.


Ralentissons et installons la 3e version des tâches:


 $ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks 

Dans la suite de cette section, vous développerez plusieurs tests pour tester la fonctionnalité de «list».
Voyons cela en action pour comprendre ce que nous allons vérifier:


Remarque du traducteur: lors de l'utilisation de la plate-forme Windows, j'ai rencontré plusieurs problèmes lors du test de la session ci-dessous.
  1. Un dossier doit être créé pour la base de données nommée tasks_db dans le dossier de votre utilisateur. Par exemple, c:\Users\User_1\tasks_db\
    Sinon, nous obtenons - >> FileNotFoundError: [Errno 2] Aucun fichier ou répertoire de ce type: 'c: \ Users \ User_1 // tasks_db // tasks_db.json'
  2. Utilisez des guillemets doubles au lieu d'une apostrophe. Sinon, obtenez une erreur
    'fais quelque chose de grand'
    Utilisation: les tâches ajoutent [OPTIONS] RÉSUMÉ
    Essayez "tâches ajouter -h" pour obtenir de l'aide.

    Erreur: vous avez obtenu des arguments supplémentaires inattendus (quelque chose de génial ')


 $ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat 

Cela semble assez simple. La commande tasks list affiche une liste de toutes les tâches sous l'en-tête.
Le titre est imprimé même si la liste est vide. La commande affiche uniquement les données d'un propriétaire, si -o ou --owner . Et comment vérifions-nous cela? Il y a plusieurs façons, mais nous allons utiliser moki.


Les tests qui utilisent des MOK sont nécessairement des tests en boîte blanche , et nous devons examiner le code pour décider quoi et où nous battrons. Le principal point d'entrée est ici:


ch7 / tasks_proj_v2 / src / tasks / cli.py

 if __name__ == '__main__': tasks_cli() 

Ceci est juste un appel à tasks_cli() :


ch7 / tasks_proj_v2 / src / tasks / cli.py

 @click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass 

De toute évidence? Non. Mais attendez, cela devient bon (ou mauvais, selon votre point de vue). Voici l'une des commandes de list :


ch7 / tasks_proj_v2 / src / tasks / cli.py

 @tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """    .   ,      . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary)) 

Lorsque vous vous habituez à écrire du code Click, assurez-vous que ce code n'est pas si mauvais. Je ne vais pas expliquer ici quoi et comment cela fonctionne dans cette fonction, car le développement du code de ligne de commande n'est pas au centre du livre; cependant, bien que je sois presque absolument sûr d'avoir ce code correct, de toute façon, il y a toujours beaucoup de place pour l'erreur humaine. C'est pourquoi un bon ensemble de tests automatisés est important pour s'assurer que cette fonctionnalité fonctionne correctement.
Cette fonction list_tasks(owner) dépend de plusieurs autres fonctions: tasks_db() , qui est le gestionnaire de contexte, et tasks.list_tasks(owner) , qui est la fonction API.


Nous allons utiliser la mock pour mettre en place de fausses fonctions pour tasks_db() et tasks.list_tasks() . Ensuite, nous pouvons appeler la méthode list_tasks via l'interface de ligne de commande et nous assurer qu'elle appelle la fonction tasks.list_tasks() , qui fonctionne correctement et traite correctement la valeur de retour.
Pour noyer tasks_db() , regardons une vraie implémentation:


Retour

Source: https://habr.com/ru/post/fr448798/


All Articles