Using pytest with Other Tools
通常不会单独使用pytest,而是在测试环境中搭配其他工具一起使用。
本章着眼于经常与 pytest 结合使用的一些工具,以便执行有效和高效的测试。虽然本章不会面面俱到,并,但是这里讨论的工具,将会让你体验到将 pytest 与其他工具结合使用的力量。
7.1 调试器¶
pdb
模块是标准库中的Python调试器。可以使用 --pdb
选项让pytest 在问题点启动一个调试会话。
让我们先来看看可以加快调试测试失败的速度的 pytest 选项:
• --tb=[auto/long/short/line/native/no]
:控制报错信息样式。
• -v
:详细展示。
• -l
:显示堆栈跟踪旁边的本地变量。
• -lf
:只运行上次失败的测试。
• -x
:在第一次失败时停止测试会话。
• --pdb
:在失败点启动一个交互式调试会话。
在--pdb
调试会话中,可以使用以下命令:
p expr
: 等同于print expr
,用来打印 exp 的值。pp expr
: 等同于pprint expr,用来美化打印expr 的值。l
: 等同于list
,用来列出故障点和上下五行代码。l begin,end
: 等同于list begin,end
,用来列出特定的行号。a
: 等同于args
,用来打印当前函数的参数及其值。(在测试帮助函数中这很有用。)u
: 等同于up
,用来在报错信息中向上移动一级。d
: 等同于down
,用来在报错信息中向下移动一级。q
: 等同于quit
,用来退出调试会话。
其他的导航命令,比如 step
和 next
并没有那么实用,因为我们刚好就停留在assert语句附近。
💡 在ch7
目录中添加新目录pdb_demo
,在其中添加新的测试模块test_pdb.py
。
# ch7/pdb_demo/test_pdb.py
def test_failing():
msg = "test_one"
assert False
执行命令:
$ cd tests
$ pytest --pdb
F
>>>>>>>>>>>>>>>>>>>>>>>>>>>>> traceback >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
E assert False
>>>>>>>>>>>>>>>>>>>>>>>>>>>> entering PDB >>>>>>>>>>>>>>>>>>>>>>>>>>>>
>>>>>>>>>>>>> PDB post_mortem (IO-capturing turned off) >>>>>>>>>>>>>>
> d:\coding\gitees\studypytest\ch7\tests\pdb_demo\test_one.py(3)test_on
e()
-> assert False
(Pdb) l
1 def test_one():
2 msg = "test_one"
3 -> assert False
[EOF]
p expr
来查看函数中的变量值:
(Pdb) pp msg
'test_one'
(Pdb) q
====================== short test summary info =======================
FAILED test_one.py::test_one - assert False
!!!!!!!!!!!!!! _pytest.outcomes.Exit: Quitting debugger !!!!!!!!!!!!!!
更多关于使用 pdb 模块的内容详见Python官方文档。 传送门:https://docs.python.org/3/library/pdb.html
7.2 覆盖率¶
测试覆盖率pytest-cov
代码覆盖率是衡量被测代码已经被测试套件所覆盖的百分比。当运行Tasks项目的测试用例时,每个测试函数都会测试一些 Tasks 功能,但绝对不是全部。
代码覆盖工具非常适合用来评估系统的哪些部分代码被测试用例完全忽略了。Coverage.py
则是Python中评估代码覆盖率的首选工具。
可以用它来检查Tasks项目中被pytest 测试的代码。
在使用coverage.py 之前,需要安装它。这里推荐直接安装 pytest-cov
插件即可。它可以通过 pytest 调用 coverage.py,支持一些pytest 选项。而且自带了coverage,所以安装 pytest-cov 一个就完全足够了,它会自动引入coverage.py。
安装 pytest-cov:
$ pip install pytest-cov
💡 将源码tasks_proj_v2
目录,整体复制到在ch7
目录中。
让我们运行Tasks 版本2的覆盖率报告。如果当前安装了 Tasks 项目的第一个版本tasks_proj,那么卸载它,并安装v2版本。
卸载旧版本的tasks:
$ pip uninstall tasks
$ cd ch7/tasks_proj_v2
$ pip install -e .
$ pip list | findstr tasks
$ pip list | findstr tasks
现在,新版的tasks已经安装完毕,可以运行来获得一份基线覆盖率报告。
执行命令:
$ cd ch7/tasks_proj_v2
$ pytest --cov=src
输出结果:
======================== test session starts =========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0
rootdir: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2, configfile: to
x.ini
plugins: cov-3.0.0, mock-3.7.0, nice-0.1.0
collected 62 items
tests\func\test_add.py ... [ 4%]
tests\func\test_add_variety.py ............................ [ 50%]
tests\func\test_add_variety2.py ............ [ 69%]
tests\func\test_api_exceptions.py ......... [ 83%]
tests\func\test_unique_id.py . [ 85%]
tests\unit\test_cli.py ..... [ 93%]
tests\unit\test_task.py .... [100%]
----------- coverage: platform win32, python 3.8.8-final-0 -----------
Name Stmts Miss Cover
--------------------------------------------------
src\tasks\__init__.py 2 0 100%
src\tasks\api.py 79 22 72%
src\tasks\cli.py 52 14 73%
src\tasks\config.py 18 12 33%
src\tasks\tasksdb_pymongo.py 74 74 0%
src\tasks\tasksdb_tinydb.py 32 4 88%
--------------------------------------------------
TOTAL 257 126 51%
========================= 62 passed in 0.44s =========================
覆盖率相对较高一些的模块是api.py(72%)和 tasksdb_tinydb.py(88%)。让我们看看tasksdb_tinydb.py
还缺少什么。
我们还可以使用 --cov-report=html
参数,生成一个HTML 报告。
执行命令:
$ pytest --cov=src --cov-report=html
然后在浏览器中打开htmlcov/index.html的效果是:
单击列表中的tasksdb_tinydb.py
将展示单个模块的覆盖率报告。如下图所示:
报告顶部显示覆盖行数的百分比,以及覆盖行数和未覆盖的行数。向下滚动,可以看到未覆盖的代码行,红色高亮部分。从中不难看出:
- 没有测试
update()
和delete()
方法。 - 没有充分测试
unique_id()
和list_tasks()
方法。
OK,这样一来,我们可以把这些列入测试待办事项。
虽然代码覆盖率工具非常有用,但是追求100% 的覆盖率是非常不现实的。当你看到未经测试的代码时,这可能意味着需要进行测试。但这也可能意味着系统的某些功能是不需要的,可以删除。像所有的软件开发工具一样,代码覆盖率分析并不能代替我们的人脑思考。
更多关于coverage.py 和 pytest-cov 知识详见各自的官方文档: https://coverage.readthedocs.io/ https://pytest-cov.readthedocs.io
7.3 替身¶
mock,Swapping Out Part of the System
mock
包用于移除系统的个别部分,以便将测试中的代码与系统的其他部分隔离开来。Mock对象有时被称为测试替身或桩。从pytest的monkeypatch
猴子补丁夹具到 mock
,都是有用的替身功能。
这个 mock
包在 Python 3.3版本以 unittest.mock
的形式发布,成为Python 标准库的一部分。在早期版本中,它是一个可以独立安装的包。这意味着从python2.6再到最新的Python 版本的 mock ,都能获得相同的功能。
当然,对于 pytest 来说,有一个叫 pytest-mock
的插件,因其便捷性推荐作为mock系统接口的首选。
对于Tasks项目,我们将使用 mock
来帮助我们测试CLI
命令行界面,毕竟我们的 cli.py 文件(英文版原书129页)根本没有被测试,现在是时候开始解决这个问题。
在之前的章节主要是对Tasks 项目的 api.py 进行了功能测试。命令行测试不是完整的功能测试所必需的。如果我们在 CLI 测试期间mock API 层,我们将有足够的信心认为系统将通过 CLI 工作。
Tasks的 CLI 的实现使用第三方Click
命令行界面库。有许多实现 CLI 的替代方案,包括 python 的内置 argparse
模块。我选择 Click 的原因之一,是因为它包含了一个测试运行器来帮助我们测试 Click 应用程序。
更多关于Click的知识,详见官方文档。 http://click.pocoo.org/
💡 在当前系统用户目录下创建tasks_db
目录,并在其中创建tasks_db.json
空文件。
注意:如果没有执行上述操作,可能会报错FileNotFoundError: [Errno 2] No such file or directory: 'C:\\Users\\xxx/tasks_db//tasks_db.json'
。
安装第2版的tasks包:
$ cd ch7
$ pip install -e tasks_proj_v2
列出tasks清单,执行的命令:
$ tasks list
ID owner done summary
-- ----- ---- -------
添加task,依次执行命令:
$ tasks add "do something great"
$ tasks add "repeat" -o Brian
$ tasks add "again and again" --owner Okken
然后列出tasks清单,执行命令:
$ 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
-o
或--owner
选项,它只打印一个owner的记录内容。
那我们怎么测试它?有很多可能的方法,但是这里使用 mock替身。
使用模拟的测试必然是白盒测试,我们必须查看代码来决定模拟什么,以及在哪里进行。
主要的入口在这里:
# ch7/tasks_proj_v2/src/tasks/cli.py
# 代码片段
# ...
if __name__ == '__main__':
tasks_cli()
这是对tasks_cli()
的调用。
# ch7/tasks_proj_v2/src/tasks/cli.py
# 代码片段
# The main entry point for tasks.
@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):
"""
List tasks in db.
If owner given, only list tasks with that 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()
,让我们看看实际的实现代码:
# ch7/tasks_proj_v2/src/tasks/cli.py
# 代码片段
@contextmanager
def _tasks_db():
config = tasks.config.get_config()
tasks.start_tasks_db(config.db_path, config.db_type)
yield
tasks.stop_tasks_db()
_tasks_db()
函数是一个上下文管理器,它通过tasks.config.get_config()
从另一个外部依赖项查询配置,并使用配置连接数据库。yield将控制释放到list_tasks()
的with代码块(cli.py的第48行) ,当所有事情都完成之后,数据库连接就断开。
为了测试CLI 行为直到调用 API 函数,我们不需要与实际数据库的连接。因此,我们可以用一个简单的替身来mock上下文管理器:
# ch7/tasks_proj_v2/tests/unit/test_cli.py
# 代码片段
from click.testing import CliRunner
from contextlib import contextmanager
import pytest
from tasks.api import Task
import tasks.cli
import tasks.config
@contextmanager
def stub_tasks_db():
yield
from contextlib import contextmanager
。
我们将使用mock 技术制造替身,来替换真正的上下文管理器。实际上,我们将使用 pytest-mock 插件提供的mock装置。
下面是一个调用tasks list的测试:
# ch7/tasks_proj_v2/tests/unit/test_cli.py
# 代码片段
def test_list_no_args(mocker):
mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)
mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
runner = CliRunner()
runner.invoke(tasks.cli.tasks_cli, ['list'])
tasks.cli.tasks.list_tasks.assert_called_once_with(None)
mocker.patch.object(tasks.cli, 'tasks_db', new=stub_tasks_db)
将 _tasks_db()
上下文管理器替换为不做任何事情的替身。
mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
,用一个空列表返回值替换从tasks.cli 中对默认 MagicMock 对象的 tasks.list_tasks()
的任何调用。我们可以在以后使用这个对象来检查它是否被正确调用。
MagicMock
类是unittest.Mock的一个灵活的子类,它具有默认行为和指定返回值的能力,这就是我们在这个例子中使用的。Mock 和 MagicMock 类(以及其他类)用于模拟其他代码的接口,内置了默认方法。
第7~8行使用了Click的CliRunner(),执行与在命令行上调用 tasks list相同的操作。
第就行使用mock 对象来确保正确调用了 API 。assert_called_once_with()
方法是unittest.mock.Mock
对象的一部分。
这些对象的更多内容详见Python文档。 传送门:https://docs.python.org/dev/library/unittest.mock.html
让我们来看一个几乎相同的检查输出的测试函数:
# ch7/tasks_proj_v2/tests/unit/test_cli.py
# 代码片段
@pytest.fixture()
def no_db(mocker):
mocker.patch.object(tasks.cli, '_tasks_db', new=stub_tasks_db)
def test_list_print_empty(no_db, mocker):
mocker.patch.object(tasks.cli.tasks, 'list_tasks', return_value=[])
runner = CliRunner()
result = runner.invoke(tasks.cli.tasks_cli, ['list'])
expected_output = (" ID owner done summary\n"
" -- ----- ---- -------\n")
assert result.output == expected_output
这一次,我们将 _tasks_db
的mock替身,放到一个no_db()的mocker夹具中,这样我们可以在以后的测试中更容易地复用它。对tasks.list_tasks()
的mock和之前一样。然而,这一次,我们也通过result.output
检查命令行操作的输出,并断言与 expected_output
相等。
这个assert 断言可以放在第一个测试test_list_no_args 中,我们可以不在需要写两个测试了。然而,与其他代码相比,我对自己获得CLI 代码正确性的能力没有那么大的信心,所以将“API 被正确调用了吗?”和“动作打印正确吗?”分成两个测试用例似乎是更为恰当的。
tasks list功能的其余测试没有添加任何新的概念,但是看看其中的几个,可能会让代码更容易理解。
# ch7/tasks_proj_v2/tests/unit/test_cli.py
# 代码片段
def test_list_print_many_items(no_db, mocker):
many_tasks = (
Task('write chapter', 'Brian', True, 1),
Task('edit chapter', 'Katie', False, 2),
Task('modify chapter', 'Brian', False, 3),
Task('finalize chapter', 'Katie', False, 4),
)
mocker.patch.object(tasks.cli.tasks, 'list_tasks',
return_value=many_tasks)
runner = CliRunner()
result = runner.invoke(tasks.cli.tasks_cli, ['list'])
expected_output = (" ID owner done summary\n"
" -- ----- ---- -------\n"
" 1 Brian True write chapter\n"
" 2 Katie False edit chapter\n"
" 3 Brian False modify chapter\n"
" 4 Katie False finalize chapter\n")
assert result.output == expected_output
def test_list_dash_o(no_db, mocker):
mocker.patch.object(tasks.cli.tasks, 'list_tasks')
runner = CliRunner()
runner.invoke(tasks.cli.tasks_cli, ['list', '-o', 'brian'])
tasks.cli.tasks.list_tasks.assert_called_once_with('brian')
def test_list_dash_dash_owner(no_db, mocker):
mocker.patch.object(tasks.cli.tasks, 'list_tasks')
runner = CliRunner()
runner.invoke(tasks.cli.tasks_cli, ['list', '--owner', 'okken'])
tasks.cli.tasks.list_tasks.assert_called_once_with('okken')
执行测试:
$ cd ch7/tasks_proj_v2/tests
$ pytest unit/test_cli.py -v
======================== test session starts =========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0 -- d:\system
\envs\mytasks\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2, configfile: to
x.ini
plugins: cov-3.0.0, mock-3.7.0, nice-0.1.0
collected 5 items
unit\test_cli.py::test_list_no_args PASSED [ 20%]
unit\test_cli.py::test_list_print_empty PASSED [ 40%]
unit\test_cli.py::test_list_print_many_items PASSED [ 60%]
unit\test_cli.py::test_list_dash_o PASSED [ 80%]
unit\test_cli.py::test_list_dash_dash_owner PASSED [100%]
========================= 5 passed in 0.04s ==========================
更多关于mock的知识,详见标准库文档中的
unittest.mock
,以及插件pytest-mock
。 传送门:https://docs.python.org/dev/library/unittest.mock.html 传送门:https://pypi.python.org/pypi/pytest-mock
7.4 多种配置¶
测试多种配置tox
tox
是一个命令行工具,允许我们在多种环境中运行完整的测试套件。我们将用它在多个Python 版本中测试 Tasks 项目。当然,tox 并不仅仅局限于 Python 版本,还可以用它来测试不同的依赖的配置,和不同的操作系统配置。
总的来说,这里是关于tox 如何工作的思维模型:
tox 使用测试包的 setup.py 文件来创建包的可安装的源码包,在 tox.ini 中查找环境列表,然后查找每个环境。
tox
会在.tox
目录中创建一个虚拟环境。tox
执行pip
安装一些依赖项。tox
使用pip
从sdist
安装包,如步骤1中所述。tox
运行测试用例。
当所有的环境都经过测试之后,tox 在测试报告中总结测试结果。
我们需要对Tasks项目做的第一件事,是添加一个 tox.ini
配置文件,与 setup.py
放在同级目录,把pytest.ini 中的所有内容都移到 tox.ini 中。
让我们看看如何修改Tasks项目,以便使用 tox 来测试python2.7
和3.6
两个版本。如果你已经安装的是其他不同的python版本,那么修改 envlist 这行,从而匹配你所安装的,或者愿意安装的python版本。
[tox]
envlist = py27,py37
下面是简化的项目结构:
tasks_proj_v2/
├── ...
├── setup.py
├── tox.ini
├── src
│ └── tasks
│ ├── __init__.py
│ ├── api.py
│ └── ...
└── tests
├── conftest.py
├── func
│ ├── __init__.py
│ ├── test_add.py
│ └── ...
└── unit
├── __init__.py
├── test_task.py
└── ...
现在的 tox.ini
文件内容是这样的:
# tox.ini , put in same dir as setup.py
[tox]
envlist = py27,py36
[testenv]
deps=pytest
commands=pytest
[pytest]
addopts = -rsxX -l --tb=short --strict
markers =
smoke: Run the smoke test test functions
get: Run the test functions that test tasks.get()
[ tox ]
下面的envlist = py27,py36
这句。它是一个简写,告诉tox使用 python2.7和 python3.6版本运行测试。在
[ testenv ]
下的deps=pytest
,告诉tox 确保 pytest
已安装。如果你有多个测试依赖项,你可以把它们写在不同的行上。也可以指定使用具体的版本。commands=pytest
告诉tox 在每个环境中运行 pytest。在
[ pytest ]
下,可以是通常想要写在 pytest.ini 的内容来配置 pytest。
python2.7下载地址:https://www.python.org/downloads/release/python-270/ python3.6下载地址:https://www.python.org/downloads/release/python-360/
在运行tox之前,必须确保已经安装了它。当前演示的tox版本是tox-3.25.0
。
$ pip install tox
然后运行tox,只需要输入tox:
$ cd ch7/tasks_proj_v2
$ tox
GLOB sdist-make: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2\setup.p
y
py27 create: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2\.tox\py27
ERROR: InterpreterNotFound: python2.7
py38 inst-nodeps: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2\.tox\.
tmp\package\1\tasks-0.1.1.zip
py38 installed: atomicwrites==1.4.0,attrs==21.4.0,click==7.1.2,colorama
==0.4.4,iniconfig==1.1.1,packaging==21.3,pluggy==1.0.0,py==1.11.0,pypar
sing==3.0.8,pytest==7.1.2,pytest-mock==3.7.0,six==1.16.0,tasks @ file:/
//D:/Coding/Gitees/studypytest/ch7/tasks_proj_v2/.tox/.tmp/package/1/ta
sks-0.1.1.zip,tinydb==3.15.1,tomli==2.0.1
py38 run-test-pre: PYTHONHASHSEED='977'
py38 run-test: commands[0] | pytest
======================== test session starts =========================
platform win32 -- Python 3.8.8, pytest-7.1.2, pluggy-1.0.0
cachedir: .tox\py38\.pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch7\tasks_proj_v2, configfile: to
x.ini
plugins: mock-3.7.0
collected 62 items
tests\func\test_add.py ... [ 4%]
tests\func\test_add_variety.py ............................ [ 50%]
tests\func\test_add_variety2.py ............ [ 69%]
tests\func\test_api_exceptions.py ......... [ 83%]
tests\func\test_unique_id.py . [ 85%]
tests\unit\test_cli.py ..... [ 93%]
tests\unit\test_task.py .... [100%]
========================= 62 passed in 0.31s =========================
______________________________ summary _______________________________
Tox 远比我在这里展示的功能强大得多,如果你使用pytest 来测试打算在多种环境中运行的软件包,那么值得学习。
更多关于tox的知识,详见官方文档。 传送门:https://tox.readthedocs.io
7.5 持续集成¶
Jenkins CI:自动化你的自动化测试
持续集成(CI)系统,例如Jenkins,通常用来在每次代码提交之后运行测试套件。Pytest 包含了一些选项,可以用来生成 Jenkins 和其他 CI 系统所需的 junit.xml 格式的文件来展示测试结果。
Jenkins 是一个经常用于持续集成的免费开源的自动化服务器。尽管 Python 不需要编译,但是我们会经常使用 Jenkins 或其他 CI 系统,来自动化 Python 项目的运行和收集测试报告。在本节中,我们将看到如何在Jenkins 中设置 Tasks 项目。
更多关于 Jenkins 的安装和使用方法,参见Jenkins网站的相关文档。 传送门:https://jenkins.io
在使用Jenkins 运行 pytest 套件时,有一些 Jenkins 插件可能会起很大作用,比如:
• build-name-setter
插件:
支持定制每次构建的显示名称,而不再是默认的#1、#2、#这种。
• Test Results Analyzer plugin
插件:
支持以表格或图形格式,显示测试执行结果的历史记录。
可以通过访问Jenkins Web页面来安装插件,地址是 ip/Manage
,然后依次单击菜单 Manage Jenkins-> Manage Plugins-> Available
。使用过滤器框搜索你想要的插件。勾选你想要的插件。通常选择“ Install without Restart”,然后在 Installing Plugins/Upgrades 页面上,选择“ Restart Jenkins when installation is complete and no jobs are running”框。
在Jenkins中创建一个名为tasks的自由风格的项目,如下图所示。
我们需要对项目进行参数化,告诉每个测试会话,在哪里安装Tasks项目,以及在哪里寻找测试函数。我们将使用一些字符串参数,来指定这些目录,如下图所示。
向下滚动到Build Environment
,选择 Delete workspace before Build starts
,并set Build Name
,将构建名称设置为 ${start_tests_dir} #${BUILD_NUMBER}
,如下图所示。
设置构建Build。在 Mac 或类 unix 系统中,选择 Add build step-> Execute shell
。在 Windows 上,选择 Add build step-> Execute Windows batch
命令。
当前演示使用的是 Mac,如下图所示:
文本框的内容如下:
# your paths will be different
code_path=/Users/okken/projects/book/bopytest/Book/code
run_tests=${code_path}/ch7/jenkins/run_tests.bash
bash -e ${run_tests} ${tasks_proj_dir} ${start_tests_dir} ${WORKSPACE}
推荐使用一个脚本,而不是将所有这些代码全部放在Jenkins
的执行块中,这样就可以通过版本控制跟踪修改记录。
💡 在ch7
目录中添加新目录jenkins_demo
,添加新的配置文件run_tests.bash
:
#!/bin/bash
# your paths will be different
top_path=/Users/okken/projects/book/bopytest/Book
code_path=${top_path}/code
venv_path=${top_path}/venv
tasks_proj_dir=${code_path}/$1
start_tests_dir=${code_path}/$2
results_dir=$3
# click and Python 3,
# from http://click.pocoo.org/5/python3/
export LC_ALL=en_US.utf-8
export LANG=en_US.utf-8
# virtual environment
source ${venv_path}/bin/activate
# install project
pip install -e ${tasks_proj_dir}
# run tests
cd ${start_tests_dir}
pytest --junit-xml=${results_dir}/results.xml
注意最后面的一行 pytest --junit-xml=${results_dir}/results.xml
,这个--junit-xml
选项是生成 Jenkins 需要的 junit.xml 格式的结果文件所唯一必需的东西。
关于junit方面的,还有其他的pytest选项。
windows系统执行命令:
$ pytest --help | findstr junit
$ pytest --help | grep junit
--junit-xml=path create junit-xml style report file at given path.
--junit-prefix=str prepend prefix to classnames in junit-xml output
junit_suite_name (string):
junit_logging (string):
junit_log_passing_tests (bool):
junit_duration_report (string):
junit_family (string):
--junit-prefix
前缀可以用于每个测试。这在使用tox时很有用,并且可以区分不同的环境的测试结果。 junit_suite_name
是一个配置文件选项,可以在pytest.ini 或 tox.ini 的[pytest]部分中设置。稍后我们会看到结果将展示from (pytest)
。如果想要将 pytest 替换为其他名称,可以指定junit_suite_name
。
接下来,我们将添加一个构建后操作:Add post-build action
->Publish Junit test result report
。
在Test report XMLs
中填写 results.xml
,如下图所示。
点击保存。
现在我们可以通过 Jenkins 运行测试了。以下是具体步骤:
- 回到到顶部项目。
- 单击
Build with Parameters
- 选择你的目录,然后点击
Build
。 - 完成后,单击构建名称并选择
Console Output
- 查看输出,试着找出出错的地方。
也许可以跳过第五步和第六步,如果能确保脚本一次性通过。在脚本中通常会出现目录权限问题、路径问题或者拼写错误等等,所以检查输出日志是个不错的习惯。
我们再运行一个版本,让它变得更有意思一点。再次点击 Build with Parameters
。这一次,保持同样的项目目录,但是设置 ch2
作为 start_tests_dir
的值,然后单击Build
构建。刷新项目顶部视图之后,你应该会看到下图所示:
单击图表内部或Latest Test Result
链接,查看测试会话的概述,并展开+
图标展开测试失败信息。
点击任何一个失败的测试名称,都会显示单独的测试失败信息,如下图所示。在这里可以看到(from pytest)
作为测试名称的一部分。这是由配置文件中的 junit_suite_name
选项所控制的。
回到Jenkins
> tasks
,你可以单击Test Results Analyzer
查看一个视图,该视图列出了哪些测试没有在不同的会话运行,以及通过/失败的状态,参见下图:
我们已经了解了如何使用Jenkins 提供的虚拟环境运行 pytest 套件,但是还有很多其他主题需要一起使用 pytest 和 Jenkins 来探讨。可以通过为每个环境设置独立的 Jenkins 任务,或者让 Jenkins 直接调用 tox 来测试多个环境。
还有一个很好的插件叫做 Cobertura
,它可以显示coverage.py
中的覆盖率数据。
更多关于
Cobertura
插件文档详见Jenkins文档。 传送门:https://wiki.jenkins-ci.org/display/JENKINS/Cobertura+Plugin
7.6 遗留测试¶
使用 pytest 运行遗留测试
unittest
是构建在 Python 标准库中的测试框架。它的目的是测试 Python 本身,但它也经常用于项目测试。pytest
作为一个 unittest 运行程序,可以在同一个会话中运行 pytest 和 unittest 测试。
让我们假设当 Tasks 项目启动时,它使用了 unittest,而不是 pytest 来进行测试。也许有很多测试用例已经写好了。幸运的是,你可以使用pytest 来运行基于unittest的测试。如果将测试工作从 unittest 迁移到 pytest,这可能是一个合理的选择。
可以把所有旧的unittest测试用例继续保留,并在 pytest 中编写新的测试。也可以在有时间的情况下,或者需要更改的时候,再逐渐迁移旧的测试。然而,在迁移过程中可能会遇到一些问题,我将在这里解决其中的一些问题。
首先,让我们看一个用 unittest 编写的测试用例。
💡 在ch7
目录中添加新目录unittest_demo
,添加新的测试模块test_delete_unittest.py
:
import unittest
import shutil
import tempfile
import tasks
from tasks import Task
def setUpModule():
"""Make temp dir, initialize DB."""
global temp_dir
temp_dir = tempfile.mkdtemp()
tasks.start_tasks_db(str(temp_dir), 'tiny')
def tearDownModule():
"""Clean up DB, remove temp dir."""
tasks.stop_tasks_db()
shutil.rmtree(temp_dir)
class TestNonEmpty(unittest.TestCase):
def setUp(self):
tasks.delete_all() # start empty
# add a few items, saving ids
self.ids = []
self.ids.append(tasks.add(Task('One', 'Brian', True)))
self.ids.append(tasks.add(Task('Two', 'Still Brian', False)))
self.ids.append(tasks.add(Task('Three', 'Not Brian', False)))
def test_delete_decreases_count(self):
# GIVEN 3 items
self.assertEqual(tasks.count(), 3)
# WHEN we delete one
tasks.delete(self.ids[0])
# THEN count decreases by 1
self.assertEqual(tasks.count(), 2)
test_delete_decreases_count()
,其余代码是前置和后置处理。
这个测试用例在unittest
中运行良好,例如执行命令:
$ cd ch7/unittest_demo
$ python -m unittest test_delete_unittest.py -v
test_delete_decreases_count (test_delete_unittest.TestNonEmpty) ... ok
----------------------------------------------------------------------
Ran 1 test in 0.018s
OK
(mytasks) D:\Coding\Gitees\studypytest\ch7\unittest_demo>
如果使用pytest运行,照样也OK:
$ cd ch7/unittest_demo
$ pytest test_delete_unittest.py -v
======================== test session starts =========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0 -- d:\system
\envs\mytasks\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch7\unittest_demo
plugins: cov-3.0.0, mock-3.7.0, nice-0.1.0
collected 1 item
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count PASS
ED [100%]
========================= 1 passed in 0.04s ==========================
如果你只想使用pytest 作为 unittest 的测试运行程序,那么这样做非常好。然而,我们的前提是 Tasks 项目正在迁移到 pytest。假设我们希望一次迁移一个测试用例,并同时运行 unittest 和 pytest 版本,直到我们对 pytest 版本有信心为止。
让我们重写这个测试模块,然后尝试同时运行它们。
💡 在ch7/unittest_demo
目录中添加新的本地插件文件conftest.py
:
# ch7/unittest_demo/conftest.py
import pytest
import tasks
from tasks import Task
@pytest.fixture()
def tasks_db(tmpdir):
"""Connect to db before tests, disconnect after."""
# Setup : start db
tasks.start_tasks_db(str(tmpdir), 'tiny')
yield # this is where the testing happens
# Teardown : stop db
tasks.stop_tasks_db()
@pytest.fixture()
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()
def db_with_3_tasks(tasks_db, tasks_just_a_few):
"""Connected db with 3 tasks, all unique."""
for t in tasks_just_a_few:
tasks.add(t)
💡 在ch7/unittest_demo
目录中添加新的测试模块test_delete_pytest.py
:
# ch7/unittest_demo/test_delete_pytest.py
import tasks
def test_delete_decreases_count(db_with_3_tasks):
ids = [t.id for t in tasks.list_tasks()]
# GIVEN 3 items
assert tasks.count() == 3
# WHEN we delete one
tasks.delete(ids[0])
# THEN count decreases by 1
assert tasks.count() == 2
db_with_3_tasks
夹具(详见第四章)会在测试之前建立数据库。
这两项测试分别通过:
$ pytest -q test_delete_pytest.py
. [100%]
1 passed in 0.03s
执行命令:
$ pytest -q test_delete_unittest.py
. [100%]
1 passed in 0.02s
我们甚至可以把它们放在一起运行,当且仅当确保unittest版本先得到执行。
执行命令:
$ pytest -v test_delete_unittest.py test_delete_pytest.py
======================== test session starts =========================
platform win32 -- Python 3.8.8, pytest-7.1.1, pluggy-1.0.0 -- d:\system
\envs\mytasks\scripts\python.exe
cachedir: .pytest_cache
rootdir: D:\Coding\Gitees\studypytest\ch7\unittest_demo
plugins: cov-3.0.0, mock-3.7.0, nice-0.1.0
collected 2 items
test_delete_unittest.py::TestNonEmpty::test_delete_decreases_count PASS
ED [ 50%]
test_delete_pytest.py::test_delete_decreases_count PASSED [100%]
========================= 2 passed in 0.05s ==========================
使用pytest 运行 unittest 的限制是,pytest测试子集将在第一次失败时停止,而unittest 将运行每个子测试,不管是否失败。当所有的子集测试通过时,pytest 会运行所有的子测试。由于这个限制,我们不会看到任何假象结果,我认为这是一个微小的差异。
相信你肯定已经准备用你自己的项目去尝试pytest了,祝你好运!
英文版原书网站:https://pragprog.com/titles/bopytest 原书论坛:https://forums.pragprog.com/forums/438