使用pytest进行Python测试。 第3章pytest固定装置

返回 下一个


本书是每本全面的Python书籍中都缺少的章节。


弗兰克·鲁伊斯
Box,Inc.首席站点可靠性工程师



本书中的示例是使用Python 3.6和pytest 3.2编写的。 pytest 3.2支持Python 2.6、2.7和Python 3.3+


本书网页上的链接 pragprog.com上提供了Tasks项目以及本书中显示的所有测试的源代码。 您无需下载源代码即可了解测试代码。 示例中以方便的形式提供了测试代码。 但是,为了跟上项目的任务,或者改编测试示例来测试自己的项目(不费力气!),您必须转到本书的网页并下载工作。 在该书的网页上,有一个勘误信息链接和一个论坛

在剧透下方是该系列文章的列表。



既然您已经了解了pytest的基础知识,那么让我们将注意力转向固定装置,这些固定装置对于构建几乎所有非平凡的软件系统的测试代码都是必不可少的。 夹具是pytest在实际测试功能之前(有时是之后)执行的功能。 灯具代码可以执行您需要的任何操作。 您可以使用夹具来获取数据集进行测试。 您可以在运行测试之前使用Fixtures使系统处于已知状态。 夹具也用于获取多个测试的数据。


这是返回数字的简单夹具示例:


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 

@pytest.fixture()装饰器用于告诉pytest该函数是一个固定装置。 当您在测试功能的参数列表中包含灯具名称时,pytest会在运行测试之前知道如何运行它。 夹具可以完成这项工作,也可以将数据返回到测试功能。


test_some_data()测试将test_some_data()名称some_data作为参数。 pytest会检测到这一点并找到具有该名称的灯具。 该名称在pytest中有意义。 pytest将在测试模块中查找具有该名称的灯具。 如果他在conftest.py中找不到它,他还将搜索。


在开始研究夹具(和conftest.py文件)之前,我需要考虑以下事实:术语夹具在编程和测试社区,甚至在Python社区中都有许多含义。 我可以交替使用fixturefixture functionfixture method来引用本章中介绍的@pytest.fixture()函数。 夹具也可以用于指示夹具功能所引用的资源。 夹具功能通常会设置或检索测试可以使用的一些数据。 有时,这些数据被认为是夹具。 例如,Django社区经常使用固定装置来指示一些在应用程序开始时加载到数据库中的原始数据。


不管其他含义如何,在pytest和本书中,测试装置都是指pytest提供的机制,用于将“准备就绪”和“清理后”代码与测试功能分开。


pytest固定装置是使pytest超越其他测试环境的独特功能之一,也是许多受人尊敬的人转而选择…并坚持使用pytest的原因。 但是,pytest中的装置与Django中的装置不同,并且与unittest和鼻子中的设置和拆卸程序不同。 固定装置有许多功能和细微差别。 一旦有了关于它们如何工作的良好心理模型,您就会感觉好些。 但是,您需要与它们玩一会儿才能进入,所以让我们开始吧。


通过conftest.py共享装置


您可以将固定装置放在单独的测试文件中,但是要在多个测试文件中共享固定装置,最好在公共位置集中使用conftest.py文件,集中进行所有测试。 对于任务项目,所有固定装置都将位于tasks_proj/tests/conftest.py


从那里,任何面团都可以共享固定装置。 如果希望仅在此文件的测试中使用灯具,则可以将灯具放在单独的测试文件中。 同样,您可以在顶部测试目录的子目录中拥有其他conftest.py文件。 如果这样做,这些低级conftest.py文件中定义的固定装置将可用于此目录和子目录中的测试。 但是,到目前为止,Tasks项目中的装置都是针对任何测试而设计的。 因此,在测试根目录conftest.py文件中使用我们所有的工具tasks_proj/tests最有意义。


尽管conftest.py是Python模块,但不应由测试文件导入。 何时不输入conftest! conftest.py文件由pytest读取,并被视为本地插件,当我们在第95页第5章“插件”中开始谈论插件时,该文件将变得很清楚。现在,将tests/conftest.py视为放置灯具的地方测试目录中的所有测试。 然后,让我们对task_proj进行一些测试,以正确使用固定装置。


使用治具进行设置和拆卸


Tasks项目中的大多数测试都假定Tasks数据库已经配置,正在运行并且已经准备就绪。 如果需要清洁,我们必须在最后删除一些条目。 并且您可能还需要断开与数据库的连接。 幸运的是,大多数任务已在任务代码中通过使用tasks.start_tasks_db(<directory to store db\>, 'tiny' or 'mongo')tasks.stop_tasks_db() 。 我们只需要在正确的时间调用它们,我们还需要一个临时目录。


幸运的是,pytest包含了一个出色的夹具,称为tmpdir。 我们可以将其用于测试,而不必担心清理。 这不是魔术,只是最好奇的人的良好编码习惯。 (请放心;我们将分析tmpdir并在第71页上的“使用tmpdir和tmpdir_factory”部分中使用tmpdir_factory进行更详细地编写。)


考虑到所有这些组件,此灯具非常有效:


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() 

tmpdir值不是字符串,而是一个代表目录的对象。 但是,它实现了__str__ ,因此我们可以使用str()将字符串传递给start_tasks_db() 。 目前,我们仍在TinyDB中使用tiny。


夹具功能在使用它的测试之前运行。 但是,如果函数具有yield ,则它将在此处停止,控制将传递给测试,并在测试完成后执行yield之后的下一行。 因此,将yield上方的代码视为“ setup”,并将yield之后的代码视为“ teardown”。 不管测试期间发生什么,都将执行yield “ teardown”之后的代码。 我们不会在此灯具中返回带有输出的数据。 但是可以。


让我们修改我们的tasks.add()测试之一以使用此固定装置:


ch3 / a test_add

 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) 

此处的主要更改是文件中多余的固定装置已被删除,并且我们在测试参数列表中添加了tasks_db 。 我喜欢使用注释构造GIVEN / WHEN / THEN格式(DANO / WHEN / AFTER)的测试,尤其是如果这从代码中看不出来的话。 我认为这在这种情况下很有用。 希望GIVEN初始化的db任务将有助于找出为什么使用tasks_db作为测试工具。




确保已安装任务。




我们仍在本章中为Tasks项目编写测试,该项目最初是在第2章中安装的。如果您跳过了本章,请确保使用cd代码安装任务; pip install ./tasks_proj/




使用–setup-show跟踪夹具执行


如果您从上一节开始运行测试,则不会看到正在运行的灯具:


 $ 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 ============ 

在设计灯具时,我需要查看什么有效以及何时可用。 幸运的是,pytest提供了这样的命令行标志-- setup-show ,它可以做到:


 $ 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 ==================== 

我们的测试在中间,而pytest为每个灯具指定了SETUP和TEARDOWN部分。 从test_add_returns_valid_id开始,您会看到tmpdir在测试之前起作用。 在那之前tmpdir_factorytmpdir似乎tmpdir将其用作固定装置。


灯具名称前面的FS表示该区域。 F表示作用域, S表示会话作用域。 我将在第56页的“示波器夹具规格”部分介绍示波器。


使用夹具测试数据


夹具是存储数据进行测试的好地方。 您可以退货。 这是一个返回混合型元组的夹具:


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 

由于test_a_tuple()应该失败(23!= 32) ,我们将看到夹具测试失败时会发生什么:


 $ 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 =========================== 

pytest与堆栈跟踪部分一起显示了导致异常或断言失败的函数的值参数。 在测试的情况下,夹具是测试的参数,因此使用堆栈跟踪报告它们。 如果在夹具中发生断言(或异常)会怎样?


 $ 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 =========================== 

有几件事发生。 堆栈跟踪正确显示断言发生在夹具函数中。 此外, test_other_data报告为FAIL ,而是报告为ERROR 。 这是一个主要区别。 如果测试突然失败,您将知道失败发生在测试本身中,并且与某些固定装置无关。


但是任务项目呢? 对于Tasks项目,我们可能会使用一些数据固定装置,可能使用具有不同属性的不同任务列表:


ch3 / a / tasks_proj /测试/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')) 

您可以直接从测试或其他固定装置中使用它们。 让我们创建非空数据库以在其帮助下进行测试。


使用多个灯具


您已经看到tmpdir使用tmpdir_factory。 您在我们的task_db固定装置中使用了tmpdir。 让我们继续进行链接,并为任务项目的非空基础添加一些专用固定装置:


ch3 / a / tasks_proj /测试/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) 

所有这些装置都在其参数列表中包括两个装置: tasks_db和数据集。 数据集用于将任务添加到数据库。 现在,如果您希望测试从非空数据库开始,则可以使用测试,例如:


ch3 / a / tasks_proj /测试/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 

这也说明了使用固定装置的主要原因之一:将测试重点放在您实际测试的内容上,而不是为准备测试做些什么。 我喜欢对GIVEN / WHEN / THEN使用注释,并出于两个原因尝试将尽可能多的数据(GIVEN)推送到固定装置中。 首先,它使测试更具可读性,因此更易于维护。 其次,夹具中的断言或异常会导致错误(ERROR),而测试功能中的断言或异常会导致错误(FAIL)。 如果数据库初始化test_add_increases_count()我不希望test_add_increases_count()失败。 这只是令人困惑。 我希望只有在add ()确实无法更改计数器的情况下, test_add_increases_count()的失败(FAIL) test_add_increases_count()可能。 让我们运行并查看所有灯具如何工作:


 $ 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 =========================== 

在功能和会话区域,我们又得到了一堆Fs和Ss。 让我们看看它是什么。


示波器夹具规格


灯具包括一个名为scope的可选参数,该参数确定灯具接收安装和拆卸的频率。 @ pytest.fixture()scope参数可以具有函数,类,模块或会话值。 范围是默认情况下的功能。 task_db设置和所有装置都尚未定义区域。 因此,它们是功能性固定装置。


以下是每个范围值的简要说明:


  • 范围='功能'


    对于测试的每个功能都执行一次。 在使用夹具进行每次测试之前,将运行设置部分。 每次使用夹具测试后,拆卸部分就会开始。 如果未指定scope参数,则这是默认区域。


  • 范围='类别'


    无论该类中有多少种测试方法,它都会对每个测试类执行一次。


  • 范围='模块'


    无论使用该模块多少次测试功能,方法或其他固定装置,每个模块都会执行一次。


  • 范围=“会话”


    每个会话执行一次。 所有使用会话范围固定装置的测试方法和功能都使用单个设置和拆卸调用。



实际作用域范围如下所示:


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.""" 

让我们使用--setup-show来演示根据区域执行的具夹具和设置调用与拆卸的配对次数:


 $ 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 =========================== 

现在,您不仅可以看到FS表示功能和会话,而且还可以看到CM表示类和模块。


范围是使用灯具定义的。 我知道这从代码中是显而易见的,但这是确保完全completely吟的重要一点。了解“)。 范围是在灯具定义中定义的,而不是在其调用位置定义的。 使用夹具的测试功能无法控制设置(SETUP)和夹具损坏(TEARDOWN)的频率。


灯具只能依靠来自相同或更多扩展范围的其他灯具。 因此,功能范围固定装置可能依赖于其他功能范围固定装置(默认情况下,仍在Tasks项目中使用)。 功能范围固定装置也可能取决于会话区域的类,模块和固定装置,但绝不会相反。


任务项目固定装置的更改范围


有了对范围的了解,现在让我们更改Task项目的某些固定装置的范围。


到目前为止,我们在测试时间上没有任何问题。 但是,您必须承认,为每个测试创建一个临时目录和一个新的数据库连接是没有用的。 只要我们可以提供一个空的数据库,则在必要时就足够了。


要将tasks_db之类的tasks_db用作会话范围,必须使用tmpdir_factory ,因为tmpdir是函数的范围,而tmpdir_factory是会话的范围。 幸运的是,这只是一行代码更改(嗯,如果在参数列表中考虑tmpdir->tmpdir_factorytmpdir->tmpdir_factory两行):


ch3 / b / tasks_proj /测试/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() 

在这里,我们根据tasks_db_session更改了tasks_db_session ,并删除了所有条目以确保其为空。 由于我们尚未更改其名称,因此已经包含它的所有固定装置或测试都不应更改。


数据固定装置只是返回一个值,因此实际上没有理由让它们一直工作。 每个会话一次就足够了:


ch3 / b / tasks_proj /测试/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')) 

现在,让我们看看所有这些更改是否都适用于我们的测试:


 $ 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 =================== 

一切似乎井井有条。 让我们看一下单个测试文件的固定装置,以了解不同区域如何根据我们的期望进行工作:


 $ 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 =========================== 

是的 . 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 ==================== 

! 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_taskrequest.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 . , .




安装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 ============= 

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


练习题


  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 . 有什么变化?
  7. 6 return <data> yield <data> .
  8. yield .
  9. pytest -s -v test_fixtures.py . ?

接下来是什么


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


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


返回 下一个

Source: https://habr.com/ru/post/zh-CN448786/


All Articles