Test Python avec pytest. CHAPITRE 3 Appareils pytest

Retour Suivant


Ce livre est le chapitre manquant qui manque dans tous les livres Python complets.


Frank ruiz
Ingénieur principal en fiabilité de site, Box, Inc.



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.



Maintenant que vous avez vu les bases de pytest, tournons notre attention vers les fixtures, qui sont nécessaires pour structurer le code de test pour presque n'importe quel systÚme logiciel non trivial. Les luminaires sont des fonctions exécutées par pytest avant (et parfois aprÚs) les fonctions de test réelles. Le code du luminaire peut faire tout ce dont vous avez besoin. Vous pouvez utiliser Fixtures pour obtenir un ensemble de données à tester. Vous pouvez utiliser des appareils pour obtenir le systÚme dans un état connu avant d'exécuter le test. Les appareils sont également utilisés pour obtenir des données pour plusieurs tests.


Voici un exemple de luminaire simple qui renvoie un nombre:


ch3 / test_fixtures.py

 import pytest @pytest.fixture() def some_data(): """Return answer to ultimate question.""" return 42 def test_some_data(some_data): """Use fixture return value in a test.""" assert some_data == 42 

Le @pytest.fixture() est utilisé pour indiquer à pytest que la fonction est un appareil. Lorsque vous incluez le nom du luminaire dans la liste des paramÚtres de la fonction de test, pytest sait comment l'exécuter avant d'exécuter le test. Les appareils peuvent faire le travail ou renvoyer des données à une fonction de test.


Le test test_some_data() a le nom du test_some_data() some_data comme paramÚtre. pytest le détectera et trouvera un appareil portant ce nom. Le nom est significatif dans pytest. pytest recherchera un appareil portant ce nom dans le module de test. Il cherchera également dans conftest.py s'il ne le trouve pas ici.


Avant de commencer nos recherches sur les fixtures (et le fichier conftest.py), je dois considĂ©rer le fait que le terme fixture a de nombreuses significations dans la communautĂ© de programmation et de test et mĂȘme dans la communautĂ© Python. J'utilise de fixture method interchangeable les fixture function fixture , fixture function et fixture method faire rĂ©fĂ©rence aux fonctions @pytest.fixture() dĂ©crites dans ce chapitre. Fixture peut Ă©galement ĂȘtre utilisĂ© pour indiquer une ressource rĂ©fĂ©rencĂ©e par une fonction fixture. Les fonctions de fixture configurent ou rĂ©cupĂšrent souvent certaines donnĂ©es avec lesquelles le test peut fonctionner. Parfois, ces donnĂ©es sont considĂ©rĂ©es comme fixes. Par exemple, la communautĂ© Django utilise souvent des appareils pour indiquer certaines des donnĂ©es brutes qui sont chargĂ©es dans la base de donnĂ©es au dĂ©but de l'application.


Indépendamment d'autres significations, dans pytest et dans ce livre, les montages de test se réfÚrent au mécanisme que pytest fournit pour séparer le code «se préparer pour» et «nettoyer aprÚs» de vos fonctions de test.


Les luminaires pytest sont l'une des caractéristiques uniques qui élÚvent le pytest par rapport aux autres environnements de test et sont la raison pour laquelle de nombreuses personnes respectées passent à ... et restent avec pytest. Cependant, les appareils dans pytest sont différents des appareils dans Django et différents des procédures de configuration et de démontage trouvées dans unittest et nose. Il existe de nombreuses fonctionnalités et nuances en ce qui concerne les luminaires. Une fois que vous aurez un bon modÚle mental de leur fonctionnement, vous vous sentirez mieux. Cependant, vous devez jouer avec eux pendant un certain temps pour entrer, alors commençons.


Partager des appareils via conftest.py


Vous pouvez placer des appareils dans des fichiers de test séparés, mais pour partager des appareils dans plusieurs fichiers de test, il est préférable d'utiliser le fichier conftest.py quelque part dans un endroit commun, de maniÚre centralisée pour tous les tests. Pour un projet de tùche, tous les appareils seront dans le tasks_proj/tests/conftest.py .


De lĂ , les appareils peuvent ĂȘtre partagĂ©s par n'importe quelle pĂąte. Vous pouvez placer les appareils dans des fichiers de test distincts si vous souhaitez que l'appareil soit utilisĂ© uniquement dans les tests de ce fichier. De mĂȘme, vous pouvez avoir d'autres fichiers conftest.py dans les sous-rĂ©pertoires du rĂ©pertoire de tests supĂ©rieur . Si vous le faites, les appareils dĂ©finis dans ces fichiers conftest.py de bas niveau seront disponibles pour les tests dans ce rĂ©pertoire et sous-rĂ©pertoires. Cependant, jusqu'Ă  prĂ©sent, les appareils du projet TĂąches ont Ă©tĂ© conçus pour n'importe quel test. Par consĂ©quent, l'utilisation de tous nos outils dans le fichier conftest.py Ă  la racine du test, tasks_proj/tests , est la plus logique.


Bien que conftest.py soit un module Python, il ne doit pas ĂȘtre importĂ© par des fichiers de test. N'importez pas conftest quand! Le fichier conftest.py est lu par pytest et considĂ©rĂ© comme un plug-in local, ce qui deviendra clair lorsque nous commencerons Ă  parler des plug-ins dans le chapitre 5 "Plug-ins" Ă  la page 95. Pour l'instant, considĂ©rez tests/conftest.py comme l'endroit oĂč nous pouvons mettre des appareils Ă  utiliser tous les tests dans le rĂ©pertoire test. task_proj ensuite certains de nos tests pour que task_proj utilise correctement les appareils.


Utilisation des appareils pour la configuration et le démontage


La plupart des tests du projet TĂąches supposent que la base de donnĂ©es TĂąches est dĂ©jĂ  configurĂ©e, en cours d'exĂ©cution et prĂȘte. Et nous devons supprimer certaines entrĂ©es Ă  la fin, s'il y a un besoin de nettoyage. Et vous devrez peut-ĂȘtre Ă©galement vous dĂ©connecter de la base de donnĂ©es. Heureusement, la plupart de ces tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo') Ă©tĂ© pris en charge dans le code de tĂąche avec tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo') et tasks.stop_tasks_db() ; nous avons juste besoin de les appeler au bon moment, et nous avons Ă©galement besoin d'un rĂ©pertoire temporaire.


Heureusement, pytest comprend un excellent appareil appelé tmpdir. Nous pouvons l'utiliser pour des tests et n'avons pas à nous soucier du nettoyage. Ce n'est pas magique, juste une bonne pratique de codage des personnes les plus curieuses. (Ne vous inquiétez pas, nous analyserons tmpdir et l'écrirons plus en détail à l'aide de tmpdir_factory dans la section «Utilisation de tmpdir et tmpdir_factory» à la page 71.)


Compte tenu de tous ces composants, ce luminaire fonctionne trĂšs bien:


ch3 / a / tasks_proj /tests/conftest.py

 import pytest import tasks from tasks import Task @pytest.fixture() def tasks_db(tmpdir): """    ,  .""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield #    # Teardown : stop db tasks.stop_tasks_db() 

La valeur de tmpdir n'est pas une chaßne - c'est un objet qui représente un répertoire. Cependant, il implémente __str__ , nous pouvons donc utiliser str() pour obtenir la chaßne à passer à start_tasks_db() . Pour l'instant, nous utilisons encore Tiny pour TinyDB.


La fonction fixture est exĂ©cutĂ©e avant les tests qui l'utilisent. Cependant, si la fonction a un rendement , elle s'arrĂȘtera lĂ , le contrĂŽle sera passĂ© aux tests et la ligne suivante aprĂšs l'exĂ©cution du rendement une fois les tests terminĂ©s. Par consĂ©quent, considĂ©rez le code ci-dessus comme "setup" et le code aprĂšs comme "dĂ©montage". Le code aprĂšs le «dĂ©montage» du rendement s'exĂ©cutera indĂ©pendamment de ce qui se passe pendant les tests. Nous ne renvoyons pas de donnĂ©es avec sortie dans ce luminaire. Mais tu peux.


tasks.add() un de nos tests tasks.add() pour utiliser ce luminaire:


ch3 / a / tasks_proj / tests / func / test_add .py

 import pytest import tasks from tasks import Task def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>)    .""" # GIVEN    # WHEN    # THEN  task_id  int new_task = Task('do something') task_id = tasks.add(new_task) assert isinstance(task_id, int) 

Le principal changement ici est que le luminaire supplémentaire dans le fichier a été supprimé, et nous avons ajouté tasks_db à la liste des paramÚtres de test. J'aime structurer les tests au format GIVEN / WHEN / THEN (DANO / WHEN / AFTER), en utilisant des commentaires, surtout si ce n'est pas évident dans le code ce qui se passe. Je pense que cela est utile dans ce cas. Espérons que les tùches de base de données initialisées tasks_db aideront à découvrir pourquoi la tasks_db utilisée comme outil de test.




Assurez-vous que Tùches est installé.




Nous écrivons toujours des tests pour le projet Tùches dans ce chapitre, qui a d'abord été installé dans le chapitre 2. Si vous avez ignoré ce chapitre, assurez-vous d'installer les tùches avec le code cd; pip install ./tasks_proj/ .




ExĂ©cution du dispositif de traçage avec –setup-show


Si vous exécutez le test à partir de la derniÚre section, vous ne verrez pas quels appareils fonctionnent:


 $ cd /path/to/code/ $ pip install ./tasks_proj/ #      $ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest -v test_add.py -k valid_id ===================== test session starts ====================== collected 3 items test_add.py::test_add_returns_valid_id PASSED ====================== 2 tests deselected ====================== ============ 1 passed, 2 deselected in 0.02 seconds ============ 

Quand je conçois des luminaires, j'ai besoin de voir ce qui fonctionne et quand. Heureusement, pytest fournit un tel indicateur de ligne de commande, -- setup-show , qui fait exactement cela:


 $ pytest --setup-show test_add.py -k valid_id ============================= test session starts ============================= collected 3 items / 2 deselected test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tmpdir, tmpdir_factory). TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory =================== 1 passed, 2 deselected in 0.18 seconds ==================== 

Notre test est au milieu, et pytest a désigné la partie SETUP et TEARDOWN pour chaque appareil. En commençant par test_add_returns_valid_id , vous voyez que tmpdir fonctionné avant le test. Et avant cela, tmpdir_factory . tmpdir semble tmpdir utiliser comme un appareil.


F et S devant les noms des luminaires indiquent la zone. F pour la portée et S pour la portée de la session. Je vais couvrir la portée dans la section «Spécifications de la fixation de la portée» à la page 56.


Utilisation d'appareils pour les données de test


Les appareils sont un excellent endroit pour stocker des données pour les tests. Vous pouvez retourner n'importe quoi. Voici un appareil qui renvoie un tuple de type mixte:


ch3 / test_fixtures.py

 @pytest.fixture() def a_tuple(): """ -  """ return (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" assert a_tuple[3]['bar'] == 32 

Puisque test_a_tuple() devrait échouer (23! = 32) , nous verrons ce qui se passe lorsque le test du luminaire échoue:


 $ cd /path/to/code/ch3 $ pytest test_fixtures.py::test_a_tuple ============================= test session starts ============================= collected 1 item test_fixtures.py F [100%] ================================== FAILURES =================================== ________________________________ test_a_tuple _________________________________ a_tuple = (1, 'foo', None, {'bar': 23}) def test_a_tuple(a_tuple): """Demo the a_tuple fixture.""" > assert a_tuple[3]['bar'] == 32 E assert 23 == 32 test_fixtures.py:38: AssertionError ========================== 1 failed in 0.17 seconds =========================== 

Avec la section de trace de pile, pytest affiche les paramÚtres de valeur de la fonction qui a provoqué l'exception ou l'échec de l'assertion. Dans le cas des tests, les appareils sont les paramÚtres du test, ils sont donc signalés à l'aide de la trace de pile. Que se passe-t-il si l'assertion (ou l'exception) se produit dans le montage?


 $ pytest -v test_fixtures.py::test_other_data ============================= test session starts ============================= test_fixtures.py::test_other_data ERROR [100%] =================================== ERRORS ==================================== ______________________ ERROR at setup of test_other_data ______________________ @pytest.fixture() def some_other_data(): """Raise an exception from fixture.""" x = 43 > assert x == 42 E assert 43 == 42 test_fixtures.py:21: AssertionError =========================== 1 error in 0.13 seconds =========================== 

Deux ou trois choses se produisent. La trace de pile montre correctement que l'assertion s'est produite dans la fonction de fixture. De plus, test_other_data signalĂ© non pas comme FAIL , mais comme ERROR . C'est une diffĂ©rence majeure. Si le test Ă©choue soudainement, vous savez que la panne s'est produite dans le test lui-mĂȘme et ne dĂ©pend pas d'un appareil.


Mais qu'en est-il du projet Tùches? Pour le projet Tùches, nous pourrions probablement utiliser certains appareils de données, éventuellement différentes listes de tùches avec différentes propriétés:


ch3 / a / tasks_proj / tests / conftest.py

 #    Task constructor # Task(summary=None, owner=None, done=False, id=None) # summary    # owner  done   # id    @pytest.fixture() def tasks_just_a_few(): """    .""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture() def tasks_mult_per_owner(): """     .""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel')) 

Vous pouvez les utiliser directement à partir de tests ou d'autres appareils. Créons des bases de données non vides pour les tests avec leur aide.


Utilisation de plusieurs appareils


Vous avez déjà vu que tmpdir utilise tmpdir_factory. Et vous avez utilisé tmpdir dans notre appareil task_db. Continuons la chaßne et ajoutons quelques appareils spécialisés pour les bases non vides du projet de tùches:


ch3 / a / tasks_proj / tests / conftest.py

 @pytest.fixture() def db_with_3_tasks(tasks_db, tasks_just_a_few): """   3 ,  .""" for t in tasks_just_a_few: tasks.add(t) @pytest.fixture() def db_with_multi_per_owner(tasks_db, tasks_mult_per_owner): """   9 , 3 owners,  3   .""" for t in tasks_mult_per_owner: tasks.add(t) 

Tous ces appareils incluent deux appareils dans leur liste de paramÚtres: tasks_db et ensemble de données. Un jeu de données est utilisé pour ajouter des tùches à la base de données. Les tests peuvent désormais les utiliser si vous souhaitez que le test démarre avec une base de données non vide, par exemple:


ch3 / a / tasks_proj / tests / func / test_add.py

 def test_add_increases_count(db_with_3_tasks): """Test tasks.add()    tasks.count().""" # GIVEN db  3  # WHEN     tasks.add(Task('throw a party')) # THEN    1 assert tasks.count() == 4 

Cela montre également l'une des principales raisons d'utiliser des appareils: pour concentrer le test sur ce que vous testez réellement, plutÎt que sur ce que vous avez dû faire pour vous préparer au test. J'aime utiliser les commentaires pour GIVEN / WHEN / THEN et essayer de pousser autant de données (GIVEN) que possible dans les appareils pour deux raisons. PremiÚrement, cela rend le test plus lisible et donc plus facile à maintenir. DeuxiÚmement, une assertion ou une exception dans un appareil entraßne une erreur (ERROR), tandis qu'une assertion ou une exception dans une fonction de test entraßne une erreur (FAIL). Je ne veux pas que test_add_increases_count() si l'initialisation de la base de données a échoué. C'est juste déroutant. Je veux que l'échec (FAIL) de test_add_increases_count() soit possible que si add () ne pouvait vraiment pas changer le compteur. Courons et voyons comment fonctionnent tous les appareils:


 $ cd /path/to/code/ch3/a/tasks_proj/tests/func $ pytest --setup-show test_add.py::test_add_increases_count ============================= test session starts ============================= collected 1 item test_add.py SETUP S tmpdir_factory SETUP F tmpdir (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tmpdir) SETUP F tasks_just_a_few SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_just_a_few, tmpdir, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_just_a_few TEARDOWN F tasks_db TEARDOWN F tmpdir TEARDOWN S tmpdir_factory ========================== 1 passed in 0.20 seconds =========================== 

Nous avons de nouveau obtenu un tas de F et de S pour la zone de fonction et de session. Voyons voir ce que c'est.


Spécifications de l'appareil de portée


Les appareils incluent un paramÚtre facultatif appelé scope , qui détermine la fréquence à laquelle les appareils reçoivent la configuration et le torndown. Le paramÚtre de portée de @ pytest.fixture() peut avoir des valeurs de fonction, de classe, de module ou de session. La portée est une fonction par défaut. Les paramÚtres tasks_db et tous les appareils ne définissent pas encore de zone. Ce sont donc des luminaires fonctionnels.


Voici une brÚve description de chaque valeur d' étendue :


  • scope = 'fonction'


    Il est effectué une fois pour chaque fonction du test. La partie configuration est exécutée avant chaque test à l'aide du luminaire. La partie démontage démarre aprÚs chaque test à l'aide du luminaire. Il s'agit de la zone par défaut si le paramÚtre d'étendue n'est pas spécifié.


  • scope = 'class'


    Il est exécuté une fois pour chaque classe de test, quel que soit le nombre de méthodes de test dans la classe.


  • scope = 'module'


    Il est exécuté une fois pour chaque module, quel que soit le nombre de fonctions ou de méthodes de test ou d'autres appareils utilisés lors de l'utilisation du module.


  • scope = 'session'


    Il est effectué une fois par session. Toutes les méthodes et fonctions de test qui utilisent des appareils de portée de session utilisent un seul appel de configuration et de démontage.



Voici à quoi ressemblent les valeurs de portée en action:


ch3 / test_scope.py

 """Demo fixture scope.""" import pytest @pytest.fixture(scope='function') def func_scope(): """A function scope fixture.""" @pytest.fixture(scope='module') def mod_scope(): """A module scope fixture.""" @pytest.fixture(scope='session') def sess_scope(): """A session scope fixture.""" @pytest.fixture(scope='class') def class_scope(): """A class scope fixture.""" def test_1(sess_scope, mod_scope, func_scope): """   ,   .""" def test_2(sess_scope, mod_scope, func_scope): """     .""" @pytest.mark.usefixtures('class_scope') class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun.""" 

Utilisons --setup-show pour démontrer que le nombre d'appels d'installation et de configuration associés à la suppression sont effectués en fonction de la zone:


 $ cd /path/to/code/ch3/ $ pytest --setup-show test_scope.py ============================= test session starts ============================= collected 4 items test_scope.py SETUP S sess_scope SETUP M mod_scope SETUP F func_scope test_scope.py::test_1 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP F func_scope test_scope.py::test_2 (fixtures used: func_scope, mod_scope, sess_scope). TEARDOWN F func_scope SETUP C class_scope test_scope.py::TestSomething::()::test_3 (fixtures used: class_scope). test_scope.py::TestSomething::()::test_4 (fixtures used: class_scope). TEARDOWN C class_scope TEARDOWN M mod_scope TEARDOWN S sess_scope ========================== 4 passed in 0.11 seconds =========================== 

Vous pouvez maintenant voir non seulement F et S pour la fonction et la session, mais aussi C et M pour la classe et le module.


La portĂ©e est dĂ©finie Ă  l'aide de luminaires. Je sais que cela est Ă©vident dans le code, mais c'est un point important pour vous assurer que vous gĂ©missez complĂštement. comprendre "). La portĂ©e est dĂ©finie dans la dĂ©finition du luminaire, et non au lieu de son appel. Les fonctions de test qui utilisent des appareils ne contrĂŽlent pas la frĂ©quence (SETUP) et les arrĂȘts de l'appareil (TEARDOWN).


Les appareils ne peuvent dĂ©pendre que d'autres appareils de la mĂȘme portĂ©e ou d'une portĂ©e plus Ă©tendue. Ainsi, le dispositif de portĂ©e de fonction peut dĂ©pendre d'un autre dispositif de portĂ©e de fonction (par dĂ©faut et est toujours utilisĂ© dans le projet TĂąches). La portĂ©e de la fonction fixture peut Ă©galement dĂ©pendre de la classe, du module et des fixtures de la zone de session, mais jamais dans l'ordre inverse.


Modifier la portée des projets de tùches


Compte tenu de cette connaissance de la portée, modifions maintenant la portée de certains appareils du projet Task.


Jusqu'à présent, nous n'avons eu aucun problÚme avec le temps de test. Mais, vous devez admettre qu'il est inutile de créer un répertoire temporaire et une nouvelle connexion à la base de données pour chaque test. Tant que nous pouvons fournir une base de données vide, lorsque cela est nécessaire, cela devrait suffire.


Pour utiliser quelque chose comme tasks_db comme étendue de la session, vous devez utiliser tmpdir_factory , car tmpdir est l'étendue de la fonction et tmpdir_factory est l'étendue de la session. Heureusement, ce n'est qu'une ligne de changement de code (enfin, deux si vous considérez tmpdir->tmpdir_factory dans la liste des paramÚtres):


ch3 / b / tasks_proj / tests / conftest.py

 """Define some fixtures to use in the project.""" import pytest import tasks from tasks import Task @pytest.fixture(scope='session') def tasks_db_session(tmpdir_factory): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), 'tiny') yield tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all() 

Ici, nous avons modifié la tasks_db fonction de la tasks_db_session et nous avons supprimé toutes les entrées pour nous assurer qu'elle est vide. Puisque nous n'avons pas changé son nom, aucun des appareils ou tests qui l'incluent déjà ne devrait changer.


Les appareils de données renvoient simplement une valeur, il n'y a donc vraiment aucune raison pour qu'ils fonctionnent tout le temps. Une fois par séance suffit:


ch3 / b / tasks_proj / tests / conftest.py

 # Reminder of Task constructor interface # Task(summary=None, owner=None, done=False, id=None) # summary is required # owner and done are optional # id is set by database @pytest.fixture(scope='session') def tasks_just_a_few(): """All summaries and owners are unique.""" return ( Task('Write some code', 'Brian', True), Task("Code review Brian's code", 'Katie', False), Task('Fix what Brian did', 'Michelle', False)) @pytest.fixture(scope='session') def tasks_mult_per_owner(): """Several owners with several tasks each.""" return ( Task('Make a cookie', 'Raphael'), Task('Use an emoji', 'Raphael'), Task('Move to Berlin', 'Raphael'), Task('Create', 'Michelle'), Task('Inspire', 'Michelle'), Task('Encourage', 'Michelle'), Task('Do a handstand', 'Daniel'), Task('Write some books', 'Daniel'), Task('Eat ice cream', 'Daniel')) 

Voyons maintenant si tous ces changements fonctionneront avec nos tests:


 $ cd /path/to/code/ch3/b/tasks_proj $ pytest ===================== test session starts ====================== collected 55 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_task.py .... ================== 55 passed in 0.17 seconds =================== 

Tout semble ĂȘtre en ordre. Examinons les appareils pour un seul fichier de test pour voir comment diffĂ©rentes zones fonctionnent selon nos attentes:


 $ pytest --setup-show tests/func/test_add.py ============================= test session starts ============================= platform win32 -- Python 3.6.5, pytest-3.9.3, py-1.7.0, pluggy-0.8.0 rootdir: c:\_BOOKS_\pytest_si\bopytest-code\code\ch3\b\tasks_proj\tests, inifile: pytest.ini collected 3 items tests\func\test_add.py SETUP S tmpdir_factory SETUP S tasks_db_session (fixtures used: tmpdir_factory) SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_add_returns_valid_id (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP F tasks_db (fixtures used: tasks_db_session) func/test_add.py::test_added_task_has_id_set (fixtures used: tasks_db, tasks_db_session, tmpdir_factory). TEARDOWN F tasks_db SETUP S tasks_just_a_few SETUP F tasks_db (fixtures used: tasks_db_session) SETUP F db_with_3_tasks (fixtures used: tasks_db, tasks_just_a_few) func/test_add.py::test_add_increases_count (fixtures used: db_with_3_tasks, tasks_db, tasks_db_session, tasks_just_a_few, tmpdir_factory). TEARDOWN F db_with_3_tasks TEARDOWN F tasks_db TEARDOWN S tasks_db_session TEARDOWN S tmpdir_factory TEARDOWN S tasks_just_a_few ========================== 3 passed in 0.24 seconds =========================== 

Ouais. . tasks_db_session , task_db .


Specifying Fixtures with usefixtures


, , , . , @pytest.mark.usefixtures('fixture1', 'fixture2') . usefixtures , , . — . :


ch3/test_scope.py

 @pytest.mark.usefixtures('class_scope') class TestSomething(): """Demo class scope fixtures.""" def test_3(self): """Test using a class scope fixture.""" def test_4(self): """Again, multiple tests are more fun.""" 

usefixtures , . , , . , - usefixtures , .


autouse Fixtures That Always Get Used ( )


, , ( usefixtures ). autouse=True , . , , . :


ch3/test_autouse.py

 """ autouse fixtures.""" import pytest import time @pytest.fixture(autouse=True, scope='session') def footer_session_scope(): """    session().""" yield now = time.time() print('--') print('finished : {}'.format(time.strftime('%d %b %X', time.localtime(now)))) print('-----------------') @pytest.fixture(autouse=True) def footer_function_scope(): """     .""" start = time.time() yield stop = time.time() delta = stop - start print('\ntest duration : {:0.3} seconds'.format(delta)) def test_1(): """   .""" time.sleep(1) def test_2(): """    .""" time.sleep(1.23) 

, . :


 $ cd /path/to/code/ch3 $ pytest -v -s test_autouse.py ===================== test session starts ====================== collected 2 items test_autouse.py::test_1 PASSED test duration : 1.0 seconds test_autouse.py::test_2 PASSED test duration : 1.24 seconds -- finished : 25 Jul 16:18:27 ----------------- =================== 2 passed in 2.25 seconds =================== 

autouse . , . , .


, autouse , , tasks_db . Tasks , , , API . . , .


Fixtures


, , , . , pytest name @pytest.fixture() :


ch3/ test_rename_fixture.py

 """ fixture renaming.""" import pytest @pytest.fixture(name='lue') def ultimate_answer_to_life_the_universe_and_everything(): """  .""" return 42 def test_everything(lue): """   .""" assert lue == 42 

lue fixture , fixture_with_a_name_much_longer_than_lue . , --setup-show :


 $ pytest --setup-show test_rename_fixture.py ======================== test session starts ======================== collected 1 items test_rename_fixture.py SETUP F lue test_rename_fixture.py::test_everything (fixtures used: lue). TEARDOWN F lue ===================== 1 passed in 0.01 seconds ====================== 

, lue , pytest --fixtures . , , , :


 $ pytest --fixtures test_rename_fixture.py ======================== test session starts ======================= ... ------------------ fixtures defined from test_rename_fixture ------------------ lue Return ultimate answer. ================= no tests ran in 0.01 seconds ================= 

— . , , , , , . , lue . «Tasks»:


 $ cd /path/to/code/ch3/b/tasks_proj $ pytest --fixtures tests/func/test_add.py ======================== test session starts ======================== ... tmpdir_factory Return a TempdirFactory instance for the test session. tmpdir Return a temporary directory path object which is unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. ----------------------- fixtures defined from conftest ------------------------ tasks_db An empty tasks db. tasks_just_a_few All summaries and owners are unique. tasks_mult_per_owner Several owners with several tasks each. db_with_3_tasks Connected db with 3 tasks, all unique. db_with_multi_per_owner Connected db with 9 tasks, 3 owners, all with 3 tasks. tasks_db_session Connect to db before tests, disconnect after. =================== no tests ran in 0.01 seconds ==================== 

Super! conftest.py . tmpdir tmpdir_factory , .



[Parametrized Testing] , . 42, . . - , , :


ch3/b/tasks_proj/tests/func/test_add_variety2.py

"""Test the tasks.add() API function."""

import pytest
import tasks
from tasks import Task

tasks_to_try = (Task('sleep', done=True),
Task('wake', 'brian'),
Task('breathe', 'BRIAN', True),
Task('exercise', 'BrIaN', False))

task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done)
for t in tasks_to_try]

def equivalent(t1, t2):
"""Check two tasks for equivalence."""
return ((t1.summary == t2.summary) and
(t1.owner == t2.owner) and
(t1.done == t2.done))

, , a_task :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

@pytest.fixture(params=tasks_to_try)
def a_task(request):
""" ."""
return request.param

def test_add_a(tasks_db, a_task):
""" a_task ( ids)."""
task_id = tasks.add(a_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, a_task)

, fixture, , . . param, , params @pytest.fixture(params=tasks_to_try) .


a_task — request.param , . , , :


 $ cd /path/to/code/ch3/b/tasks_proj/tests/func $ pytest -v test_add_variety2.py::test_add_a ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_a[a_task0] PASSED test_add_variety2.py::test_add_a[a_task1] PASSED test_add_variety2.py::test_add_a[a_task2] PASSED test_add_variety2.py::test_add_a[a_task3] PASSED =================== 4 passed in 0.03 seconds =================== 

, pytest , () . , :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

@pytest.fixture(params=tasks_to_try, ids=task_ids)
def b_task(request):
""" ."""
return request.param

def test_add_b(tasks_db, b_task):
""" b_task, ."""
task_id = tasks.add(b_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, b_task)

:


 $ pytest -v test_add_variety2.py::test_add_b ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_b[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_b[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_b[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_b[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds =================== 

ids , , . , :


ch3/b/tasks_proj/tests/func/ test_add_variety2.py

def id_func(fixture_value):
""" ."""
t = fixture_value
return 'Task({},{},{})'.format(t.summary, t.owner, t.done)

@pytest.fixture(params=tasks_to_try, ids=id_func)
def c_task(request):
""" (id_func) ."""
return request.param

def test_add_c(tasks_db, c_task):
""" ."""
task_id = tasks.add(c_task)
t_from_db = tasks.get(task_id)
assert equivalent(t_from_db, c_task)

. Task, id_func() Task , namedtuple Task Task . , , :


 $ pytest -v test_add_variety2.py::test_add_c ===================== test session starts ====================== collected 4 items test_add_variety2.py::test_add_c[Task(sleep,None,True)] PASSED test_add_variety2.py::test_add_c[Task(wake,brian,False)] PASSED test_add_variety2.py::test_add_c[Task(breathe,BRIAN,True)] PASSED test_add_variety2.py::test_add_c[Task(exercise,BrIaN,False)] PASSED =================== 4 passed in 0.04 seconds =================== 

. , , . , !


Fixtures Tasks Project


, Tasks. TinyDB . , . , , , , TinyDB , MongoDB .


( ), , start_tasks_db() tasks_db_session :


ch3/b/tasks_proj/tests/conftest.py

""" ."""

import pytest
import tasks
from tasks import Task

@pytest.fixture(scope='session')
def tasks_db_session(tmpdir_factory):
""" , ."""
temp_dir = tmpdir_factory.mktemp('temp')
tasks.start_tasks_db(str(temp_dir), 'tiny')
yield
tasks.stop_tasks_db()

@pytest.fixture()
def tasks_db(tasks_db_session):
""" tasks."""
tasks.delete_all()

db_type start_tasks_db() . , :


tasks_proj/src/tasks/api.py

  def start_tasks_db(db_path, db_type): # type: (str, str) -None """  API  .""" if not isinstance(db_path, string_types): raise TypeError('db_path must be a string') global _tasksdb if db_type == 'tiny': import tasks.tasksdb_tinydb _tasksdb = tasks.tasksdb_tinydb.start_tasks_db(db_path) elif db_type == 'mongo': import tasks.tasksdb_pymongo _tasksdb = tasks.tasksdb_pymongo.start_tasks_db(db_path) else: raise ValueError("db_type   'tiny'  'mongo'") 

MongoDB, db_type mongo. :


ch3/c/tasks_proj/tests/conftest.py

  import pytest import tasks from tasks import Task # @pytest.fixture(scope='session', params=['tiny',]) @pytest.fixture(scope='session', params=['tiny', 'mongo']) def tasks_db_session(tmpdir_factory, request): """Connect to db before tests, disconnect after.""" temp_dir = tmpdir_factory.mktemp('temp') tasks.start_tasks_db(str(temp_dir), request.param) yield # this is where the testing happens tasks.stop_tasks_db() @pytest.fixture() def tasks_db(tasks_db_session): """An empty tasks db.""" tasks.delete_all() 

params=['tiny',' mongo'] -. request temp_db db_type request.param , "tiny" "mongo".


--verbose -v pytest , pytest . , .




Installation de MongoDB




MongoDB, , MongoDB pymongo . MongoDB, https://www.mongodb.com/download-center . pymongo pip— pip install pymongo . MongoDB ; 7 .




:


  $ cd /path/to/code/ch3/c/tasks_proj $ pip install pymongo $ pytest -v --tb=no ===================== test session starts ====================== collected 92 items test_add.py::test_add_returns_valid_id[tiny] PASSED test_add.py::test_added_task_has_id_set[tiny] PASSED test_add.py::test_add_increases_count[tiny] PASSED test_add_variety.py::test_add_1[tiny] PASSED test_add_variety.py::test_add_2[tiny-task0] PASSED test_add_variety.py::test_add_2[tiny-task1] PASSED ... test_add.py::test_add_returns_valid_id[mongo] FAILED test_add.py::test_added_task_has_id_set[mongo] FAILED test_add.py::test_add_increases_count[mongo] PASSED test_add_variety.py::test_add_1[mongo] FAILED test_add_variety.py::test_add_2[mongo-task0] FAILED ... ============= 42 failed, 50 passed in 4.94 seconds ============= 

Hm. . , , - Mongo. , pdb: , . 125. TinyDB.


Exercices


  1. test_fixtures.py .
    2. fixtures—functions @pytest.fixture() , . , , .
  2. , .
  3. , .
  4. pytest --setup-show test_fixtures.py . ?
  5. scope= 'module' 4.
  6. pytest --setup-show test_fixtures.py . Qu'est-ce qui a changé?
  7. 6 return <data> yield <data> .
  8. yield .
  9. pytest -s -v test_fixtures.py . ?

Et ensuite


pytest fixture , , building blocks , setup teardown , (, Mongo TinyDB). , , .


pytest, , (builtin) tmpdir tmpdir_factory. (builtin) .


Retour Suivant

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


All Articles