回去
通常,pytest不是单独使用,而是在带有其他工具的测试环境中使用。 本章讨论了通常与pytest结合使用以进行有效测试的其他工具。 尽管这绝不是一个详尽的清单,但是这里讨论的工具将使您了解将pytest与其他工具混合使用的威力。

本书中的示例是使用Python 3.6和pytest 3.2编写的。 pytest 3.2支持Python 2.6、2.7和Python 3.3+。
本书网页上的链接 pragprog.com上提供了Tasks项目以及本书中显示的所有测试的源代码。 您无需下载源代码即可了解测试代码。 示例中以方便的形式提供了测试代码。 但是,为了跟上项目的任务,或者改编测试示例来测试自己的项目(不费力气!),您必须转到本书的网页并下载工作。 在该书的网页上,有一个勘误信息链接和一个论坛 。
在剧透下方是该系列文章的列表。
pdb:调试测试失败
pdb
模块是标准库中的Python调试器。 您使用--pdb
以便pytest在出现故障时启动调试会话。 让我们来看一下Tasks项目上下文中的pdb
。
在第64页的“参数化夹具”中,我们使Tasks项目留下了一些错误:
$ cd /path/to/code/ch3/c/tasks_proj $ pytest --tb=no -q .........................................FF.FFFF FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.FFF........... 42 failed, 54 passed in 4.74 seconds
在查看pdb
如何帮助我们调试此测试之前,让我们看一下可用的pytest选项以加快调试测试错误的速度,我们首先在第9页的“使用选项”部分中进行了检查:
--tb=[auto/long/short/line/native/no]
:控制跟踪样式。-v / --verbose
:显示所有通过或失败的测试名称。-l / --showlocals
:在堆栈跟踪旁边显示局部变量。-lf / --last-failed
:仅运行失败的测试。-x / --exitfirst
:在第一次失败时停止测试会话。--pdb
:在故障点启动交互式调试会话。
安装MongoDB
如第49页的第3章“ Pytest固定装置”中所述,运行MongoDB测试需要MongoDB
和pymongo
安装。
我测试了位于https://www.mongodb.com/download-center上的社区服务器的版本。 pymongo使用pip install pymongo
: pip install pymongo
。 但是,这是使用MongoDB的书中的最后一个示例。 要在不使用MongoDB的情况下尝试调试器,可以从code/ch2/
运行pytest命令,因为该目录还包含一些失败的测试。
我们只是从code/ch3/c
运行了测试,以确保其中一些不起作用。 我们没有看到回溯或测试名称,因为--tb=no
禁用了跟踪,并且没有启用--verbose
。 让我们用详细的文本重复错误(不超过三个):
$ pytest --tb=no --verbose --lf --maxfail=3 ============================= test session starts ============================= collected 96 items / 52 deselected run-last-failure: rerun previous 44 failures tests/func/test_add.py::test_add_returns_valid_id[mongo] ERROR [ 2%] tests/func/test_add.py::test_added_task_has_id_set[mongo] ERROR [ 4%] tests/func/test_add.py::test_add_increases_count[mongo] ERROR [ 6%] =================== 52 deselected, 3 error in 0.72 seconds ====================
现在我们知道哪些测试失败了。 让我们看一下其中的一个,使用-x
,打开跟踪,不使用--tb=no
,并使用-l
显示局部变量:
$ pytest -v --lf -l -x ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED =========================== FAILURES =========================== _______________ test_add_returns_valid_id[mongo] _______________ tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783baf8204177f24cb1b68'), int) new_task = Task(summary='do something', owner=None, done=False, id=None) task_id = ObjectId('59783baf8204177f24cb1b68') tasks_db = None tests/func/test_add.py:16: AssertionError !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== =========== 1 failed, 54 deselected in 2.47 seconds ============
通常这足以了解测试失败的原因。 在这种特殊情况下,很明显task_id
不是整数-它是ObjectId的实例。 ObjectId是MongoDB在数据库中用于对象标识符的类型。 我对tasksdb_pymongo.py
层的意图是使系统其余部分隐藏MongoDB实现的某些细节。 很明显,在这种情况下,它不起作用。
但是,我们希望了解如何将pdb与pytest一起使用,因此让我们想象一下尚不清楚该测试为何失败。 我们可以使pytest启动调试会话,并使用--pdb
在故障点--pdb
:
$ pytest -v --lf -x --pdb ===================== test session starts ====================== run-last-failure: rerun last 42 failures collected 96 items tests/func/test_add.py::test_add_returns_valid_id[mongo] FAILED >>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>> tasks_db = None def test_add_returns_valid_id(tasks_db): """tasks.add(<valid task>) should return an integer.""" # GIVEN an initialized tasks db # WHEN a new task is added # THEN returned task_id is of type int new_task = Task('do something') task_id = tasks.add(new_task) > assert isinstance(task_id, int) E AssertionError: assert False E + where False = isinstance(ObjectId('59783bf48204177f2a786893'), int) tests/func/test_add.py:16: AssertionError >>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>> > /path/to/code/ch3/c/tasks_proj/tests/func/test_add.py(16) > test_add_returns_valid_id() -> assert isinstance(task_id, int) (Pdb)
现在,我们将在提示符(Pdb)的提示下,访问所有交互式pdb调试功能。 查看崩溃时,我经常使用以下命令:
p/print expr
:打印exp的值。pp expr
:漂亮打印出expr的值。l/list
:列出故障点以及上下两行代码。l/list begin,end
:枚举特定的行号。a/args
:显示当前函数的参数及其值。u/up
:将堆栈路径上移一层。d/down
:在堆栈跟踪中下移一级。q/quit
:结束调试会话。
其他导航命令(例如step和next)不是很有用,因为我们位于assert语句中。 您也可以简单地输入变量名称并获取值。
您可以与-l/--showlocals
类似地使用p/print expr
来查看函数中的值:
(Pdb) p new_task Task(summary='do something', owner=None, done=False, id=None) (Pdb) p task_id ObjectId('59783bf48204177f2a786893') (Pdb)
现在,您可以退出调试器并继续测试。
(Pdb) q !!!!!!!!!!!! Interrupted: stopping after 1 failures !!!!!!!!!!!! ===================== 54 tests deselected ====================== ========== 1 failed, 54 deselected in 123.40 seconds ===========
如果我们不使用-
,pytest将在下一个测试中再次打开Pdb。 Python文档中提供了有关使用pdb模块的更多信息。
Coverage.py:确定测试代码的数量
代码覆盖率是一组测试所测试的已测试代码百分比的指标。 在为“任务”项目运行测试时,某些“任务”功能在每次测试时都会执行,但不会全部执行。
代码覆盖率工具非常适合让您知道测试完全忽略了系统的哪些部分。
Coverage.py
是首选的Python覆盖率工具,用于测量代码的覆盖率。
您将使用它通过pytest验证Tasks项目代码。
要使用coverage.py
您需要安装它。 安装名为pytest-cov
的插件pytest-cov
没有什么坏处,该插件使您可以从pytest中调用coverage.py
并提供其他一些pytest选项。 由于coverage
是pytest-cov
的依赖项之一,因此只需安装pytest-cov
,它将采用coverage.py
:
$ pip install pytest-cov Collecting pytest-cov Using cached pytest_cov-2.5.1-py2.py3-none-any.whl Collecting coverage>=3.7.1 (from pytest-cov) Using cached coverage-4.4.1-cp36-cp36m-macosx_10_10_x86 ... Installing collected packages: coverage, pytest-cov Successfully installed coverage-4.4.1 pytest-cov-2.5.1
让我们运行第二个任务版本的覆盖率报告。 如果您仍然安装了Tasks项目的第一个版本,请卸载它并安装版本2:
$ pip uninstall tasks Uninstalling tasks-0.1.0: /path/to/venv/bin/tasks /path/to/venv/lib/python3.6/site-packages/tasks.egg-link Proceed (y/n)? y Successfully uninstalled tasks-0.1.0 $ cd /path/to/code/ch7/tasks_proj_v2 $ pip install -e . Obtaining file:///path/to/code/ch7/tasks_proj_v2 ... Installing collected packages: tasks Running setup.py develop for tasks Successfully installed tasks $ pip list ... tasks (0.1.1, /path/to/code/ch7/tasks_proj_v2/src) ...
现在已安装任务的下一版本,您可以运行基本覆盖率报告:
$ cd /path/to/code/ch7/tasks_proj_v2 $ pytest --cov=src ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Name Stmts Miss Cover -------------------------------------------------- src\tasks\__init__.py 2 0 100% src\tasks\api.py 79 22 72% src\tasks\cli.py 45 14 69% src\tasks\config.py 18 12 33% src\tasks\tasksdb_pymongo.py 74 74 0% src\tasks\tasksdb_tinydb.py 32 4 88% -------------------------------------------------- TOTAL 250 126 50% ================== 62 passed in 0.47 seconds ===================
由于当前目录为tasks_proj_v2
,并且受测试的源代码位于src中,因此添加--cov=src
选项仅针对该受测试目录生成覆盖报告。
如您所见,某些文件的覆盖率非常低,甚至为0%。 这些是有用的提醒: tasksdb_pymongo.py
0%,因为我们已禁用此版本中的MongoDB测试。 其中一些是相当低的。 在准备黄金时间之前,该项目当然必须为所有这些领域提供测试。
我相信几个文件的覆盖率更高: api.py
和tasksdb_tinydb.py
。 让我们看一下tasksdb_tinydb.py
,看看缺少了什么。 我认为做到这一点的最佳方法是使用HTML报告。
如果使用--cov-report=html
选项再次运行coverage.py
,将生成--cov-report=html
:
$ pytest --cov=src --cov-report=html ===================== test session starts ====================== plugins: mock-1.6.2, cov-2.5.1 collected 62 items tests/func/test_add.py ... tests/func/test_add_variety.py ............................ tests/func/test_add_variety2.py ............ tests/func/test_api_exceptions.py ......... tests/func/test_unique_id.py . tests/unit/test_cli.py ..... tests/unit/test_task.py .... ---------- coverage: platform darwin, python 3.6.2-final-0 ----------- Coverage HTML written to dir htmlcov ================== 62 passed in 0.45 seconds ===================
然后,您可以在浏览器中打开htmlcov/index.html
,在以下屏幕上显示输出:

单击tasksdb_tinydb.py
将显示一个文件的报告。 覆盖行的百分比显示在报告的顶部,加上覆盖的行数和未覆盖的行,如下一屏幕所示:

向下滚动,您可以看到缺失的行,如以下屏幕所示:

即使此屏幕不是该文件的完整页面,也足以告诉我们:
- 我们不使用所有者集来测试
list_tasks()
。 - 我们不测试
update()
或delete()
。 - 也许我们没有彻底测试
unique_id()
。
太好了 我们可以将它们包括在我们的TO-DO测试列表中,同时可以测试配置系统。
尽管代码覆盖率工具非常有用,但是争取100%覆盖率可能很危险。 当您看到未经测试的代码时,可能意味着需要测试。 但这也意味着有些系统功能是不需要的,可以删除。 像所有软件开发工具一样,代码覆盖率分析也不能取代思维。
有关更多详细信息,请参见 coverage.py
和pytest-cov
的 coverage.py
。
模拟:替换系统零件
模拟包用于替换系统的某些部分,以将测试代码的部分与系统的其余部分隔离开。 模拟-对象有时被称为测试双打,间谍,假货或存根。
在您自己的pytest Monkeypatch夹具(在第85页的使用Monkeypatch中所述)和模拟之间,您应该具有所有必需的双重测试功能。
注意! 模拟和很奇怪
如果这是您第一次遇到模拟,存根和间谍之类的测试双胞胎,请做好准备! 这将是非常奇怪,非常快,有趣,尽管非常令人印象深刻。
mock
包随附标准的Python库,自Python 3.3起unittest.mock
。 在早期版本中,它作为通过PyPI安装的单独软件包提供。 这意味着您可以使用Python 2.6的mock
PyPI版本到最新的Python版本,并获得与最新的mock
Python相同的功能。 但是,与pytest一起使用时,名为pytest-mock
的插件具有一些功能,使其成为我的模拟系统首选界面。
对于Tasks项目,我们将使用mock
来帮助我们测试命令行界面。 在Coverage.py中:通过确定要测试的代码量,在第129页上,您看到我们的cli.py
文件根本没有经过测试。 我们现在将开始修复它。 但是,让我们先谈谈策略。
Tasks项目中的第一个解决方案是通过api.py
进行大多数功能测试。 因此,一个合理的解决方案是命令行测试不必是完整的功能测试。 如果在CLI测试期间获得湿API级别,我们可以确保系统将通过CLI正常工作。 这也是一种方便的解决方案,可让我们在此部分中查看moki。
CLI Tasks实现使用第三方Click命令行界面包。 有许多实现命令行界面的方法,包括内置在Python argparse
的模块。 我选择Click的原因之一是因为它包含一个可帮助我们测试Click应用程序的测试引擎。 但是,尽管我们希望cli.py
的代码是Click应用程序的典型,但它并不明显。
让我们放慢速度并安装第三个版本的Tasks:
$ cd /path/to/code/ $ pip install -e ch7/tasks_proj_v2 ... Successfully installed tasks
在本节的其余部分,您将开发一些测试来测试列表的功能。
让我们看一下它的作用,以了解我们要检查的内容:
译者注:在使用Windows平台时,在测试下面的会话时遇到了几个问题。
- 应该在用户文件夹中为名为
tasks_db
的数据库创建一个文件夹。 例如c:\Users\User_1\tasks_db\
否则,我们得到->> FileNotFoundError:[Errno 2]没有这样的文件或目录:'c:\ Users \ User_1 // tasks_db // tasks_db.json' - 使用双引号而不是撇号。 否则会出现错误
“做点很棒的事”
用法:任务添加[选项]摘要
尝试“任务添加-h”以获取帮助。
错误:出现了意外的额外参数(很棒的“”)
$ tasks list ID owner done summary -- ----- ---- ------- $ tasks add 'do something great' $ tasks add "repeat" -o Brian $ tasks add "again and again" --owner Okken $ tasks list ID owner done summary -- ----- ---- ------- 1 False do something great 2 Brian False repeat 3 Okken False again and again $ tasks list -o Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat $ tasks list --owner Brian ID owner done summary -- ----- ---- ------- 2 Brian False repeat
看起来很简单。 tasks list
命令在标题下显示所有任务的列表。
即使列表为空,也会打印标题。 如果使用-o
或--owner
,则该命令仅显示来自一个所有者的数据。 以及我们如何检查呢? 有很多方法,但是我们将使用moki。
使用MOK的测试必定是白盒测试 ,我们需要研究代码来确定要击打的内容和位置。 主要入口点在这里:
ch7 /tasks_proj_v2/src/tasks/cli.py
if __name__ == '__main__': tasks_cli()
这只是对tasks_cli()
的调用:
ch7 /tasks_proj_v2/src/tasks/cli.py
@click.group(context_settings={'help_option_names': ['-h', '--help']}) @click.version_option(version='0.1.1') def tasks_cli(): """Run the tasks application.""" pass
显然吗? 不行 但是,等等,它会变得好(或坏,取决于您的观点)。 这是list
命令之一:
ch7 /tasks_proj_v2/src/tasks/cli.py
@tasks_cli.command(name="list", help="list tasks") @click.option('-o', '--owner', default=None, help='list tasks with this owner') def list_tasks(owner): """ . , . """ formatstr = "{: >4} {: >10} {: >5} {}" print(formatstr.format('ID', 'owner', 'done', 'summary')) print(formatstr.format('--', '-----', '----', '-------')) with _tasks_db(): for t in tasks.list_tasks(owner): done = 'True' if t.done else 'False' owner = '' if t.owner is None else t.owner print(formatstr.format( t.id, owner, done, t.summary))
当您习惯于编写Click代码时,请确保该代码还不错。 我不会在这里解释它在此功能中的作用和工作方式,因为命令行代码的开发不是本书的重点。 但是,尽管我几乎绝对可以确保我拥有正确的代码,但是总会有很大的人为错误余地。 因此,要确保此功能正常运行,必须进行一系列良好的自动化测试。
此list_tasks(owner)
函数依赖于其他几个函数: tasks_db()
是上下文管理器,而tasks.list_tasks(owner)
是API函数。
我们将使用mock
将虚假函数放置在tasks_db()
和tasks.list_tasks()
适当位置。 然后,我们可以通过命令行界面调用list_tasks
方法,并确保它调用了tasks.list_tasks()
函数,该函数可以正常工作并正确处理返回值。
要淹没tasks_db()
,让我们看一个实际的实现:
回去