使用pytest进行Python测试。 第2章,编写测试函数

返回 下一个


您将学习如何将测试组织到类,模块和目录中。 然后,我将向您展示如何使用标记来标记要运行的测试,并讨论内置标记如何帮助您跳过测试并标记测试,以防失败。 最后,我将讨论测试的参数化,该参数化允许使用不同的数据调用测试。



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


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

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



在上一章中,您运行了pytest。 您了解了如何使用文件和目录运行它以及有多少个选项起作用。 在本章中,您将学习如何在测试Python包的上下文中编写测试函数。 如果您使用pytest测试除Python软件包以外的任何内容,那么本章的大部分内容将对您有所帮助。


我们将为Tasks包编写测试。 在执行此操作之前,我将讨论Python分发包的结构及其测试,以及如何使测试能够看到测试包。 然后,我将向您展示如何在测试中使用断言,测试如何处理意外的异常以及测试预期的异常。


最后,我们将进行许多测试。 这样,您将学习如何将测试组织到类,模块和目录中。 然后,我将向您展示如何使用标记来标记要运行的测试,并讨论内置标记如何帮助您跳过测试并标记测试,以防失败。 最后,我将讨论测试的参数化,该参数化允许使用不同的数据调用测试。


译者注: 如果您使用的是Python 3.5或3.6,则在第2章中运行测试时,您可能会收到类似以下的消息

通过修复...\code\tasks_proj\src\tasks\tasksdb_tinydb.py并重新安装任务包,可以解决此问题...\code\tasks_proj\src\tasks\tasksdb_tinydb.py
 $ cd /path/to/code $ pip install ./tasks_proj/` 


eids在模块中doc_ids eidsdoc_ids命名参数eidseids上的eid ...\code\tasks_proj\src\tasks\tasksdb_tinydb.py

说明请参阅此处的 #83783

包装测试


要了解如何为Python包编写测试功能,我们将使用第xii页上的Tasks项目中所述的示例Tasks项目。 Tasks是一个Python软件包,其中包含具有相同任务名称的命令行工具。


附录4,打包和分发Python项目,第175页,介绍了如何在一个小团队内部或通过PyPI在全球范围内分发您的项目,因此,我将不做任何详细介绍。 但是,让我们快速查看一下Tasks项目中的内容以及不同文件如何适合该项目的测试历史记录。


以下是Tasks项目的文件结构:


 tasks_proj/ ├── CHANGELOG.rst ├── LICENSE ├── MANIFEST.in ├── README.rst ├── setup.py ├── src │ └── tasks │ ├── __init__.py │ ├── api.py │ ├── cli.py │ ├── config.py │ ├── tasksdb_pymongo.py │ └── tasksdb_tinydb.py └── tests ├── conftest.py ├── pytest.ini ├── func │ ├── __init__.py │ ├── test_add.py │ └── ... └── unit ├── __init__.py ├── test_task.py └── ... 

我提供了项目的完整列表(测试文件的完整列表除外),以指示测试如何适合项目的其余部分,并指向几个测试的关键文件,即conftest.py,pytest.ini和各种__init__.py文件和setup.py


所有测试都存储在测试中,并且与src中的包源文件分开。 这不是pytest的要求,但这是最佳实践。


所有顶级文件CHANGELOG.rst,LICENSE,README.rst,MANIFEST.insetup.py在第175页的附录4,打包和分发Python项目中都有更详细的讨论。尽管setup.py对于构建发行版重要从软件包中下载,以及用于在本地安装软件包的功能,以便可以导入该软件包。


功能测试和单元测试分为各自的目录。 这是一个任意决定,不是必需的。 但是,将测试文件组织到几个目录中可以轻松地运行测试的子集。 我喜欢将功能测试和单元测试分开,因为只有在我们有意更改系统功能时,功能测试才会中断,而在重构或实现更改期间,单元测试可能会中断。


该项目包含__init__.py文件的两种类型:在src/目录中找到的文件和在tests/找到的文件。 src/tasks/__init__.py告诉Python该目录是一个包。 当有人使用import tasks时,它也充当包的主要接口。 它包含用于从api.py导入某些函数的代码,因此cli.py和我们的测试文件可以访问包函数,例如tasks.add() ,而不是执行task.api.add ()tests/func/__init__.py文件tests/func/__init__.pytests/unit/__init__.py为空。 他们告诉pytest在一个目录中查找测试目录的根目录和pytest.ini文件。


pytest.ini文件是可选的。 它包含整个项目的常规pytest配置。 您的项目最多只能有一个。 它可能包含更改pytest行为的指令,例如,设置将始终使用的参数列表。 您将在第113页的第6章“配置”中了解有关pytest.ini全部信息。


conftest.py文件也是可选的。 pytest被视为“本地插件”,并且可能包含挂钩函数和装置。 挂钩函数是一种将代码嵌入pytest运行时的一部分以更改pytest的工作方式的方法。 夹具是在测试功能之前和之后运行的设置和拆卸功能,可用于表示测试使用的资源和数据。 (在第49页的第3章pytest固定装置和在第71页的第4章内置固定装置中讨论了固定装置,在第95页的第5章“插件”中讨论了挂钩函数。)多个子目录中的测试应包含在tests / conftest.py中。 您可以有几个conftest.py文件; 例如,在测试中可以有一个,而在每个测试子目录中可以有一个。


如果您尚未这样做,则可以本书的网站上下载该项目的源代码副本。 或者,您可以使用类似的结构来处理项目。


这是test_task.py:


ch2 / task_proj /测试/单元/ test_task.py

 """Test the Task data type.""" # -*- coding: utf-8 -*- from tasks import Task def test_asdict(): """_asdict()   .""" t_task = Task('do something', 'okken', True, 21) t_dict = t_task._asdict() expected = {'summary': 'do something', 'owner': 'okken', 'done': True, 'id': 21} assert t_dict == expected def test_replace(): """replace ()      .""" t_before = Task('finish book', 'brian', False) t_after = t_before._replace(id=10, done=True) t_expected = Task('finish book', 'brian', True, 10) assert t_after == t_expected def test_defaults(): """        .""" t1 = Task() t2 = Task(None, None, False, None) assert t1 == t2 def test_member_access(): """ .field  namedtuple.""" t = Task('buy milk', 'brian') assert t.summary == 'buy milk' assert t.owner == 'brian' assert (t.done, t.id) == (False, None) 

test_task.py文件包含以下导入语句:


 from tasks import Task 

让测试导入任务或从任务中导入某些东西的最佳方法是使用pip在本地安装任务。 这是可能的,因为有一个setup.py文件可直接调用pip。


通过运行pip install .安装任务pip install .pip install -e . 从task_proj目录中。 或从上一级目录运行pip install -e tasks_proj另一个选项:


 $ cd /path/to/code $ pip install ./tasks_proj/ $ pip install --no-cache-dir ./tasks_proj/ Processing ./tasks_proj Collecting click (from tasks==0.1.0) Downloading click-6.7-py2.py3-none-any.whl (71kB) ... Collecting tinydb (from tasks==0.1.0) Downloading tinydb-3.4.0.tar.gz Collecting six (from tasks==0.1.0) Downloading six-1.10.0-py2.py3-none-any.whl Installing collected packages: click, tinydb, six, tasks Running setup.py install for tinydb ... done Running setup.py install for tasks ... done Successfully installed click-6.7 six-1.10.0 tasks-0.1.0 tinydb-3.4.0 

如果只想运行任务测试,则可以执行此命令。 如果您希望能够在任务安装过程中更改源代码,则需要使用带有-e选项的安装(对于可编辑的“可编辑”):


 $ pip install -e ./tasks_proj/ Obtaining file:///path/to/code/tasks_proj Requirement already satisfied: click in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: tinydb in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Requirement already satisfied: six in /path/to/venv/lib/python3.6/site-packages (from tasks==0.1.0) Installing collected packages: tasks Found existing installation: tasks 0.1.0 Uninstalling tasks-0.1.0: Successfully uninstalled tasks-0.1.0 Running setup.py develop for tasks Successfully installed tasks 

现在尝试运行测试:


 $ cd /path/to/code/ch2/tasks_proj/tests/unit $ pytest test_task.py ===================== test session starts ====================== collected 4 items test_task.py .... =================== 4 passed in 0.01 seconds =================== 

导入有效! 现在,其他测试可以安全地使用导入任务。 现在让我们编写一些测试。


使用断言语句


在编写测试函数时,常规的Python语句assert是报告测试失败的主要工具。 pytest的简单性非常出色。 这就是使许多开发人员在其他框架之上使用pytest的原因。


如果您使用任何其他测试平台,则可能会看到各种断言助手功能。 例如,以下是一些形式的assert和assert辅助函数的列表:


pytest单元测试
断言assertTrue(某物)
断言a == bassertEqual(a,b)
断言a <= bassertLessEqual(a,b)
......

使用pytest,您可以对任何表达式使用assert <表达式>。 如果将表达式转换为bool时其计算结果为False,则测试将失败。


pytest包含一个称为断言重写的函数,该函数将拦截断言调用并将其替换为可以告诉您更多有关语句失败原因的信息。 让我们看看如果看到一些语句错误,这种重写有多么有用:


ch2 / task_proj /测试/单元/ test_task_fail.py

 """ the Task type    .""" from tasks import Task def test_task_equality(): """     .""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') assert t1 == t2 def test_dict_equality(): """ ,   dicts,    .""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() assert t1_dict == t2_dict 

所有这些测试均失败,但是跟踪中的信息很有趣:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py FF ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Use -v to get the full diff test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Use -v to get the full diff test_task_fail.py:16: AssertionError ========================== 2 failed in 0.30 seconds =========================== 

哇! 这是很多信息。 对于每个不成功的测试,将显示确切的错误字符串,并带有>失败指针。 E行显示有关断言失败的其他信息,以帮助您了解问题所在。


我故意在test_task_equality()放置了两个不匹配test_task_equality() ,但在前面的代码中仅显示了第一个不匹配test_task_equality() 。 根据错误消息中的建议,让我们再次使用-v标志:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\unit>pytest -v test_task_fail.py ============================= test session starts ============================= collected 2 items test_task_fail.py::test_task_equality FAILED test_task_fail.py::test_dict_equality FAILED ================================== FAILURES =================================== _____________________________ test_task_equality ______________________________ def test_task_equality(): """Different tasks should not be equal.""" t1 = Task('sit there', 'brian') t2 = Task('do something', 'okken') > assert t1 == t2 E AssertionError: assert Task(summary=...alse, id=None) == Task(summary='...alse, id=None) E At index 0 diff: 'sit there' != 'do something' E Full diff: E - Task(summary='sit there', owner='brian', done=False, id=None) E ? ^^^ ^^^ ^^^^ E + Task(summary='do something', owner='okken', done=False, id=None) E ? +++ ^^^ ^^^ ^^^^ test_task_fail.py:9: AssertionError _____________________________ test_dict_equality ______________________________ def test_dict_equality(): """Different tasks compared as dicts should not be equal.""" t1_dict = Task('make sandwich', 'okken')._asdict() t2_dict = Task('make sandwich', 'okkem')._asdict() > assert t1_dict == t2_dict E AssertionError: assert OrderedDict([...('id', None)]) == OrderedDict([(...('id', None)]) E Omitting 3 identical items, use -vv to show E Differing items: E {'owner': 'okken'} != {'owner': 'okkem'} E Full diff: E {'summary': 'make sandwich', E - 'owner': 'okken', E ? ^... E E ...Full output truncated (5 lines hidden), use '-vv' to show test_task_fail.py:16: AssertionError ========================== 2 failed in 0.28 seconds =========================== 

好吧,我认为这太酷了! pytest不仅能够找到这两个差异,而且还向我们确切说明了这些差异在哪里。 这个例子只使用相等断言。 您可以在pytest.org上找到assert语句的更多变体,以及令人惊叹的跟踪调试信息。


预期异常


Tasks API中的多个地方都可能发生异常。 让我们快速看一下task / api.py中的函数:


 def add(task): # type: (Task) -\> int def get(task_id): # type: (int) -\> Task def list_tasks(owner=None): # type: (str|None) -\> list of Task def count(): # type: (None) -\> int def update(task_id, task): # type: (int, Task) -\> None def delete(task_id): # type: (int) -\> None def delete_all(): # type: () -\> None def unique_id(): # type: () -\> int def start_tasks_db(db_path, db_type): # type: (str, str) -\> None def stop_tasks_db(): # type: () -\> None 

cli.py中的CLI代码和api.py中的API代码之间就将哪种类型传递给API函数达成了协议。 如果类型不正确,我希望可以在API调用中引发异常。 要确保这些函数在未正确调用的情况下引发异常,请在测试函数中使用错误的类型故意引发TypeError异常,并与pytest.raises(预期的异常)一起使用,例如:


ch2 / tasks_proj /测试/func/test_api_exceptions.py

 """    -   API.""" import pytest import tasks def test_add_raises(): """add()       param.""" with pytest.raises(TypeError): tasks.add(task='not a Task object') 

test_add_raises() ,使用pytest.raises(TypeError) :该语句报告下一段代码中的所有内容均应引发TypeError异常。 如果未引发异常,则测试将失败。 如果测试引发另一个异常,则它将失败。


我们只是在test_add_raises()检查了异常类型。 您还可以检查排除选项。 对于start_tasks_db(db_path, db_type) ,不仅db_type应该是字符串,而且实际上应该是'tiny'或'mongo'。 您可以通过添加excinfo来检查异常消息是否正确:


ch2 / tasks_proj /测试/func/test_api_exceptions.py

 def test_start_tasks_db_raises(): """,     .""" with pytest.raises(ValueError) as excinfo: tasks.start_tasks_db('some/great/path', 'mysql') exception_msg = excinfo.value.args[0] assert exception_msg == "db_type must be a 'tiny' or 'mongo'" 

这使我们可以更仔细地研究此异常。 as之后的变量名(在本例中为excinfo)中填充了异常信息,并且其类型为ExceptionInfo。


在我们的例子中,我们要确保第一个(也是唯一的)异常参数与字符串匹配。


标记测试功能


pytest提供了一种很酷的机制来将标记放入测试函数中。 一个测试可以有多个标记,并且一个标记可以处于多个测试中。


在您看到标记的作用后,标记对您便有意义。 假设我们希望将测试的子集作为快速的“烟雾测试”来了解系统中是否存在严重的差距。 按照惯例,Smoke测试不是全面的,全面的测试套件,而是可以快速运行的选定子集,可为开发人员提供有关系统所有部分运行状况的良好印象。


要将烟雾测试套件添加到Tasks项目,您需要为某些测试添加@mark.pytest.smoke 。 让我们将其添加到几个test_api_exceptions.py测试中(请注意, test_api_exceptions.py中没有内置Smoke和get标记;我只是想出了它们):


ch2 / tasks_proj /测试/func/test_api_exceptions.py

 @pytest.mark.smoke def test_list_raises(): """list()       param.""" with pytest.raises(TypeError): tasks.list_tasks(owner=123) @pytest.mark.get @pytest.mark.smoke def test_get_raises(): """get()       param.""" with pytest.raises(TypeError): tasks.get(task_id='123') 

现在,我们仅运行那些带有-m marker_name标记的测试:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd func (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED test_api_exceptions.py::test_get_raises PASSED ============================= 5 tests deselected ============================== =================== 2 passed, 5 deselected in 0.18 seconds ==================== (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func> (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ==================== 

请记住, -v --verbose缩写,它使我们能够查看正在运行的测试的名称。 使用-m'smoke'运行两个测试,标记为@ pytest.mark.smoke。


使用@pytest.mark.get '将运行一个标记为@pytest.mark.get测试。 很简单


一切都变成奇迹,奇迹! -m之后的表达式可以使用andornot组合多个标记:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_get_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ==================== 

我们只在smokeget标记的情况下进行了此测试。 我们可以not使用:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v -m "smoke and not get" test_api_exceptions.py ============================= test session starts ============================= collected 7 items test_api_exceptions.py::test_list_raises PASSED ============================= 6 tests deselected ============================== =================== 1 passed, 6 deselected in 0.13 seconds ==================== 

添加@pytest.mark.smoke -m 'smoke and not get'选择一个标记为@pytest.mark.smoke而不是@pytest.mark.get


烟雾测试填充


以前的测试似乎还不是一套合理的smoke test 。 实际上,我们没有触摸数据库,也没有添加任何任务。 当然,必须进行smoke test


让我们添加一些考虑添加任务的测试,并将其中一个用作烟雾测试套件的一部分:


ch2 / task_proj /测试/ func / test_add.py

 """  API tasks.add ().""" import pytest import tasks from tasks import Task def test_add_returns_valid_id(): """tasks.add(valid task)    .""" # 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) @pytest.mark.smoke def test_added_task_has_id_set(): """,   task_id  tasks.add().""" # GIVEN an initialized tasks db # AND a new task is added new_task = Task('sit in chair', owner='me', done=True) task_id = tasks.add(new_task) # WHEN task is retrieved task_from_db = tasks.get(task_id) # THEN task_id matches id field assert task_from_db.id == task_id 

这两个测试在初始化的任务数据库上都有GIVEN注释,但是测试中没有初始化的数据库。 我们可以定义夹具来在测试之前初始化数据库,并在测试之后进行清理:


ch2 / task_proj /测试/ func / test_add.py

 @pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """Connect to db before testing, disconnect after.""" # Setup : start db tasks.start_tasks_db(str(tmpdir), 'tiny') yield #    # Teardown : stop db tasks.stop_tasks_db() 

本示例中使用的夹具tmpdir是内置夹具。 您将在第71页的第4章,内置装置中学习有关内置装置的所有信息,并在第49页的第3章pytest装置中学习如何编写自己的装置以及它们如何工作,包括此处使用的autouse参数。


测试中使用的自动使用情况表明该文件中的所有测试都将使用fixture。 在每次测试之前执行yield之前的代码; 测试后执行yield之后的代码。 如果需要,yield可以将数据返回到测试。 在接下来的章节中,您将考虑所有这些以及更多内容,但是在这里,我们需要以某种方式配置数据库以进行测试,因此我不再需要等待,而是必须向您展示该设备(当然是设备!)。 (pytest还支持老式的设置和拆卸功能,例如在unittest鼻子中使用的功能,但并不那么有趣。但是,如果您感兴趣的话,请参见第183页的附录5 xUnit固定装置。)


现在让我们推迟对灯具的讨论,然后转到项目的开始并运行我们的烟雾测试套件


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests>cd .. (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -m "smoke" ============================= test session starts ============================= collected 56 items tests/func/test_add.py::test_added_task_has_id_set PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED ============================= 53 tests deselected ============================= =================== 3 passed, 53 deselected in 0.49 seconds =================== 

它显示了来自不同文件的标记测试可以一起运行。


跳过测试


尽管第31页的标记验证方法中讨论的标记是您选择的名称,但是pytest包含一些有用的内置标记: skipskipifxfail 。 在本节中,我将讨论skipskipif ,以及下一个-xfail


使用skipskipif可以跳过不需要执行的测试。 例如,假设我们不知道tasks.unique_id()应该如何工作。 每个电话应返回不同的号码? , ?


-, (, initialized_tasks_db ; ):


ch2/tasks_proj/tests/func/ test_unique_id_1.py

 """Test tasks.unique_id().""" import pytest import tasks def test_unique_id(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 

:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_1.py ============================= test session starts ============================= collected 1 item test_unique_id_1.py F ================================== FAILURES =================================== _______________________________ test_unique_id ________________________________ def test_unique_id(): """Calling unique_id() twice should return different numbers.""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() > assert id_1 != id_2 E assert 1 != 1 test_unique_id_1.py:11: AssertionError ========================== 1 failed in 0.30 seconds =========================== 

嗯 , . API , , docstring """Return an integer that does not exist in the db.""", , DB . . , :


ch2/tasks_proj/tests/func/ test_unique_id_2.py

 @pytest.mark.skip(reason='misunderstood the API') def test_unique_id_1(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 def test_unique_id_2(): """unique_id()    id.""" ids = [] ids.append(tasks.add(Task('one'))) ids.append(tasks.add(Task('two'))) ids.append(tasks.add(Task('three'))) #   id uid = tasks.unique_id() # ,        assert uid not in ids 

, , , @pytest..skip() .


:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_2.py ============================= test session starts ============================= collected 2 items test_unique_id_2.py::test_unique_id_1 SKIPPED test_unique_id_2.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds ===================== 

, - , , 0.2.0 . skipif:


ch2/tasks_proj/tests/func/ test_unique_id_3.py

 @pytest.mark.skipif(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id ()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 

, skipif() , Python. , , . skip , skipif . skip , skipif . ( reason ) skip , skipif xfail . :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. ===================== 1 passed, 1 skipped in 0.20 seconds ===================== 

s. , (skipped), (passed). , - -v :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py::test_unique_id_1 SKIPPED test_unique_id_3.py::test_unique_id_2 PASSED ===================== 1 passed, 1 skipped in 0.19 seconds ===================== 

. -rs :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -rs test_unique_id_3.py ============================= test session starts ============================= collected 2 items test_unique_id_3.py s. =========================== short test summary info =========================== SKIP [1] func\test_unique_id_3.py:8: not supported until version 0.2.0 ===================== 1 passed, 1 skipped in 0.22 seconds ===================== 

-r chars :


 $ pytest --help ... -r chars show extra test summary info as specified by chars (     ,  ) (f)ailed, (E)error, (s)skipped, (x)failed, (X)passed, (p)passed, (P)passed with output, (a)all except pP. ... 

, .



skip skipif , . xfail pytest , , . unique_id () , xfail :


ch2/tasks_proj/tests/func/ test_unique_id_4.py

 @pytest.mark.xfail(tasks.__version__ < '0.2.0', reason='not supported until version 0.2.0') def test_unique_id_1(): """ unique_id()     .""" id_1 = tasks.unique_id() id_2 = tasks.unique_id() assert id_1 != id_2 @pytest.mark.xfail() def test_unique_id_is_a_duck(): """ xfail.""" uid = tasks.unique_id() assert uid == 'a duck' @pytest.mark.xfail() def test_unique_id_not_a_duck(): """ xpass.""" uid = tasks.unique_id() assert uid != 'a duck' 

Running this shows:


, , xfail . == vs.! =. .


:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py xxX. =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================ 

X XFAIL, « ( expected to fail )». X XPASS «, , ( expected to fail but passed. )».


--verbose :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_unique_id_4.py ============================= test session starts ============================= collected 4 items test_unique_id_4.py::test_unique_id_1 xfail test_unique_id_4.py::test_unique_id_is_a_duck xfail test_unique_id_4.py::test_unique_id_not_a_duck XPASS test_unique_id_4.py::test_unique_id_2 PASSED =============== 1 passed, 2 xfailed, 1 xpassed in 0.36 seconds ================ 

pytest , , , xfail , FAIL. pytest.ini :


 [pytest] xfail_strict=true 

pytest.ini 6, , . 113.



, ​​ . . , , . , . . .


A Single Directory


, pytest :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest tests\func --tb=no ============================= test session starts ============================= collected 50 items tests\func\test_add.py .. tests\func\test_add_variety.py ................................ tests\func\test_api_exceptions.py ....... tests\func\test_unique_id_1.py F tests\func\test_unique_id_2.py s. tests\func\test_unique_id_3.py s. tests\func\test_unique_id_4.py xxX. ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 1.75 seconds ===== 

, -v , .


 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests\func --tb=no ============================= test session starts ============================= 

...


 collected 50 items tests\func\test_add.py::test_add_returns_valid_id PASSED tests\func\test_add.py::test_added_task_has_id_set PASSED tests\func\test_add_variety.py::test_add_1 PASSED tests\func\test_add_variety.py::test_add_2[task0] PASSED tests\func\test_add_variety.py::test_add_2[task1] PASSED tests\func\test_add_variety.py::test_add_2[task2] PASSED tests\func\test_add_variety.py::test_add_2[task3] PASSED tests\func\test_add_variety.py::test_add_3[sleep-None-False] PASSED ... tests\func\test_unique_id_2.py::test_unique_id_1 SKIPPED tests\func\test_unique_id_2.py::test_unique_id_2 PASSED ... tests\func\test_unique_id_4.py::test_unique_id_1 xfail tests\func\test_unique_id_4.py::test_unique_id_is_a_duck xfail tests\func\test_unique_id_4.py::test_unique_id_not_a_duck XPASS tests\func\test_unique_id_4.py::test_unique_id_2 PASSED ==== 1 failed, 44 passed, 2 skipped, 2 xfailed, 1 xpassed in 2.05 seconds ===== 

, .


File/Module


, , pytest:


 $ cd /path/to/code/ch2/tasks_proj $ pytest tests/func/test_add.py =========================== test session starts =========================== collected 2 items tests/func/test_add.py .. ======================== 2 passed in 0.05 seconds ========================= 

.



, :: :


 $ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_add.py::test_add_returns_valid_id =========================== test session starts =========================== collected 3 items tests/func/test_add.py::test_add_returns_valid_id PASSED ======================== 1 passed in 0.02 seconds ========================= 

-v , , .


Test Class


Here's an example:


— , .
这是一个例子:


ch2/tasks_proj/tests/func/ test_api_exceptions.py

 class TestUpdate(): """    tasks.update().""" def test_bad_id(self): """non-int id   excption.""" with pytest.raises(TypeError): tasks.update(task_id={'dict instead': 1}, task=tasks.Task()) def test_bad_task(self): """A non-Task task   excption.""" with pytest.raises(TypeError): tasks.update(task_id=1, task='not a task') 

, update() , . , , :: , :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v tests/func/test_api_exceptions.py::TestUpdate ============================= test session starts ============================= collected 2 items tests\func\test_api_exceptions.py::TestUpdate::test_bad_id PASSED tests\func\test_api_exceptions.py::TestUpdate::test_bad_task PASSED ========================== 2 passed in 0.12 seconds =========================== 

A Single Test Method of a Test Class


, — :: :


 $ cd /path/to/code/ch2/tasks_proj $ pytest -v tests/func/test_api_exceptions.py::TestUpdate::test_bad_id ===================== test session starts ====================== collected 1 item tests/func/test_api_exceptions.py::TestUpdate::test_bad_id PASSED =================== 1 passed in 0.03 seconds =================== 

,

, , , , . , pytest -v .


-k , . and , or not . , _raises :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k _raises ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_delete_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 51 tests deselected ============================= =================== 5 passed, 51 deselected in 0.54 seconds =================== 

and not test_delete_raises() :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj>pytest -v -k "_raises and not delete" ============================= test session starts ============================= collected 56 items tests/func/test_api_exceptions.py::test_add_raises PASSED tests/func/test_api_exceptions.py::test_list_raises PASSED tests/func/test_api_exceptions.py::test_get_raises PASSED tests/func/test_api_exceptions.py::test_start_tasks_db_raises PASSED ============================= 52 tests deselected ============================= =================== 4 passed, 52 deselected in 0.44 seconds =================== 

, , , -k . , , .


[Parametrized Testing]:


, , . . - pytest, - .


, , add() :


ch2/tasks_proj/tests/func/ test_add_variety.py

 """  API tasks.add().""" import pytest import tasks from tasks import Task def test_add_1(): """tasks.get ()  id,   add() works.""" task = Task('breathe', 'BRIAN', True) task_id = tasks.add(task) t_from_db = tasks.get(task_id) # ,  ,    assert equivalent(t_from_db, task) def equivalent(t1, t2): """   .""" #  ,   id return ((t1.summary == t2.summary) and (t1.owner == t2.owner) and (t1.done == t2.done)) @pytest.fixture(autouse=True) def initialized_tasks_db(tmpdir): """    ,  .""" tasks.start_tasks_db(str(tmpdir), 'tiny') yield tasks.stop_tasks_db() 

tasks id None . id . == , , . equivalent() , id . autouse , , . , :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_1 ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_1 PASSED ========================== 1 passed in 0.69 seconds =========================== 

. , . , ? . @pytest.mark.parametrize(argnames, argvalues) , :


ch2/tasks_proj/tests/func/ test_add_variety.py

 @pytest.mark.parametrize('task', [Task('sleep', done=True), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)]) def test_add_2(task): """    .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) 

parametrize() — — 'task', . — , Task. pytest :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_2 ============================= test session starts ============================= collected 4 items test_add_variety.py::test_add_2[task0] PASSED test_add_variety.py::test_add_2[task1] PASSED test_add_variety.py::test_add_2[task2] PASSED test_add_variety.py::test_add_2[task3] PASSED ========================== 4 passed in 0.69 seconds =========================== 

parametrize() . , , :


ch2/tasks_proj/tests/func/ test_add_variety.py

 @pytest.mark.parametrize('summary, owner, done', [('sleep', None, False), ('wake', 'brian', False), ('breathe', 'BRIAN', True), ('eat eggs', 'BrIaN', False), ]) def test_add_3(summary, owner, done): """    .""" task = Task(summary, owner, done) task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) 

, pytest, , :


 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3 ============================= test session starts ============================= platform win32 -- Python 3.5.2, pytest-3.5.1, py-1.5.3, pluggy-0.6.0 -- cachedir: ..\.pytest_cache rootdir: ...\bopytest-code\code\ch2\tasks_proj\tests, inifile: pytest.ini collected 4 items test_add_variety.py::test_add_3[sleep-None-False] PASSED [ 25%] test_add_variety.py::test_add_3[wake-brian-False] PASSED [ 50%] test_add_variety.py::test_add_3[breathe-BRIAN-True] PASSED [ 75%] test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 4 passed in 0.37 seconds =========================== 

, , pytest, :


 (venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_3[sleep-None-False] ============================= test session starts ============================= test_add_variety.py::test_add_3[sleep-None-False] PASSED [100%] ========================== 1 passed in 0.22 seconds =========================== 

, :


 (venv35) c:\BOOK\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_3[eat eggs-BrIaN-False]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_3[eat eggs-BrIaN-False] PASSED [100%] ========================== 1 passed in 0.56 seconds =========================== 

, :


ch2/tasks_proj/tests/func/ test_add_variety.py

 tasks_to_try = (Task('sleep', done=True), Task('wake', 'brian'), Task('wake', 'brian'), Task('breathe', 'BRIAN', True), Task('exercise', 'BrIaN', False)) @pytest.mark.parametrize('task', tasks_to_try) def test_add_4(task): """ .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) 

. :


 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_4 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_4[task0] PASSED [ 20%] test_add_variety.py::test_add_4[task1] PASSED [ 40%] test_add_variety.py::test_add_4[task2] PASSED [ 60%] test_add_variety.py::test_add_4[task3] PASSED [ 80%] test_add_variety.py::test_add_4[task4] PASSED [100%] ========================== 5 passed in 0.34 seconds =========================== 

, . , ids parametrize() , . ids , . , tasks_to_try , :


ch2/tasks_proj/tests/func/ test_add_variety.py

 task_ids = ['Task({},{},{})'.format(t.summary, t.owner, t.done) for t in tasks_to_try] @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) def test_add_5(task): """Demonstrate ids.""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) 

, :


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::test_add_5 ============================= test session starts ============================= collected 5 items test_add_variety.py::test_add_5[Task(sleep,None,True)] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)0] PASSED test_add_variety.py::test_add_5[Task(wake,brian,False)1] PASSED test_add_variety.py::test_add_5[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 5 passed in 0.45 seconds =========================== 

:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v "test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)]" ============================= test session starts ============================= collected 1 item test_add_variety.py::test_add_5[Task(exercise,BrIaN,False)] PASSED ========================== 1 passed in 0.21 seconds =========================== 

; shell. parametrize() . :


ch2/tasks_proj/tests/func/ test_add_variety.py

 @pytest.mark.parametrize('task', tasks_to_try, ids=task_ids) class TestAdd(): """   .""" def test_equivalent(self, task): """ ,   .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert equivalent(t_from_db, task) def test_valid_id(self, task): """          .""" task_id = tasks.add(task) t_from_db = tasks.get(task_id) assert t_from_db.id == task_id 

:


 (venv33) ...\bopytest-code\code\ch2\tasks_proj\tests\func>pytest -v test_add_variety.py::TestAdd ============================= test session starts ============================= collected 10 items test_add_variety.py::TestAdd::test_equivalent[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_equivalent[Task(exercise,BrIaN,False)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(sleep,None,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)0] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(wake,brian,False)1] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(breathe,BRIAN,True)] PASSED test_add_variety.py::TestAdd::test_valid_id[Task(exercise,BrIaN,False)] PASSED ========================== 10 passed in 1.16 seconds ========================== 

, @pytest.mark.parametrize() . pytest.param(<value\>, id="something") :


:


 (venv35) ...\bopytest-code\code\ch2\tasks_proj\tests\func $ pytest -v test_add_variety.py::test_add_6 ======================================== test session starts ========================================= collected 3 items test_add_variety.py::test_add_6[just summary] PASSED [ 33%] test_add_variety.py::test_add_6[summary\owner] PASSED [ 66%] test_add_variety.py::test_add_6[summary\owner\done] PASSED [100%] ================================ 3 passed, 6 warnings in 0.35 seconds ================================ 

, id .


练习题


  1. , task_proj , - , pip install /path/to/tasks_proj .
  2. .
  3. pytest .
  4. pytest , tasks_proj/tests/func . pytest , . . , ?
  5. xfail , pytest tests .
  6. tasks.count() , . API , , , .
  7. ? test_api_exceptions.py . , . ( api.py .)

接下来是什么


pytest . , , , . initialized_tasks_db . / .


他们还可以分隔通用代码,以便多个测试功能可以使用相同的设置。在下一章中,您将深入研究pytest灯具的美好世界。


返回 下一个

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


All Articles