在上一章中,我们讨论了为什么测试如此重要,并简要概述了unittest模块。我们还粗略地看了一下 pytest 的功能,但几乎没有尝到它们的味道。
在本章中,我们将从 pytest 开始我们的旅程。我们将注重实效,因此这意味着我们不会详尽地研究 pytest 可以做的所有事情,而是为您提供快速概述的基础知识,以使您快速高效。我们将研究如何编写测试,如何将它们组织到文件和目录中,以及如何有效地使用 pytest 的命令行
以下是本章介绍的内容:
- 安装 pytest
- 编写和运行测试
- 组织文件和包
- 有用的命令行选项
- 配置:
pytest.ini文件
In the chapter, there are a lot of examples typed into the command line. They are marked by the λ character. To avoid clutter and to focus on the important parts, the pytest header (which normally displays the pytest version, the Python version, installed plugins, and so on) will be suppressed.
让我们直接跳到如何安装 pytest。
安装 pyTest 非常简单,但首先,让我们花一点时间来回顾 Python 开发的良好实践。
All of the examples are for Python 3. They should be easy to adapt to Python 2 if necessary.
安装依赖项的推荐做法是创建一个virtualenv。Avirtualenv(https://packaging.python.org/guides/installing-using-pip-and-virtualenv/ )的作用类似于操作系统附带的完整的单独 Python 安装,可以安全地安装应用程序所需的软件包,而不会破坏系统 Python 或工具。
现在,我们将学习如何创建虚拟环境并使用 pip 安装 pytest。如果您已经熟悉virtualenv和 pip,您可以跳过本节:
- 在命令提示符中键入此项以创建一个
virtualenv:
λ python -m venv .env- 此命令将在当前目录中创建一个
.env文件夹,其中包含完整的 Python 安装。在继续之前,您应该activate和virtualenv:
λ source .env/bin/activate或在 Windows 上:
λ .env\Scripts\activate这将把virtualenvPython 放在$PATH环境变量前面,因此 Python、pip 和其他工具将从virtualenv执行,而不是从您的系统执行。
- 最后,要安装 pytest,请键入:
λ pip install pytest您可以通过键入以下内容来验证一切是否顺利:
λ pytest --version
This is pytest version 3.5.1, imported from x:\fibo\.env36\lib\site-packages\pytest.py现在,我们都准备好了,可以开始了!
使用 pytest,开始编写测试只需创建一个名为test_*.py的新文件,并编写以test开头的测试函数:
# contents of test_player_mechanics.py
def test_player_hit():
player = create_player()
assert player.health == 100
undead = create_undead()
undead.hit(player)
assert player.health == 80要执行此测试,只需执行pytest,传递文件名:
λ pytest test_player_mechanics.py如果您没有通过任何测试,pytest 将递归地从当前目录中查找所有测试文件,并自动执行它们。
You might encounter examples on the internet that use py.test in the command line instead of pytest. The reason for that is historical: pytest used to be part of the py package, which provided several general purpose utilities, including tools that followed the convention of starting with py.<TAB> for tab completion, but since then, it has been moved into its own project. The old py.test command is still available and is an alias to pytest, but the latter is the recommended modern usage.
注意,不需要创建类;只要简单的函数和简单的assert语句就足够了,但是如果您想使用类对测试进行分组,您可以这样做:
class TestMechanics:
def test_player_hit(self):
...
def test_player_health_flask(self):
...当您希望将多个测试放在同一范围内时,分组测试非常有用:您可以基于它们所在的类执行测试,将标记应用于类中的所有测试(第 3 章、标记和参数化),并创建绑定到类的固定装置(第 4 章、固定装置。
Pytest 可以通过多种方式运行测试。现在让我们快速进入基础,在本章的后面,我们将继续讨论更高级的选项。
只需执行pytest命令即可开始:
λ pytest这将递归地找到当前目录和下面目录中的所有test_*.py和*_test.py模块,并将运行这些文件中找到的所有测试:
- 您可以将搜索减少到特定目录:
λ pytest tests/core tests/contrib- 您还可以混合使用任意数量的文件和目录:
λ pytest tests/core tests/contrib/test_text_plugin.py- 您可以使用语法
<test-file>::<test-function-name>执行特定测试:
λ pytest tests/core/test_core.py::test_regex_matching- 您可以执行
test类的所有test方法:
λ pytest tests/contrib/test_text_plugin.py::TestPluginHooks- 您可以使用语法
<test-file>::<test-class>::<test-method-name>执行test类的特定test方法:
λ pytest tests/contrib/
test_text_plugin.py::TestPluginHooks::test_registration上面使用的语法是 pytest 内部创建的,对于收集的每个测试都是唯一的,称为node id或item id它基本上由::字符连接在一起的测试模块、类和函数的文件名组成。
*Pytest 将显示更详细的输出,其中包括节点 ID,并带有-v标志:
λ pytest tests/core -v
======================== test session starts ========================
...
collected 6 items
tests\core\test_core.py::test_regex_matching PASSED [ 16%]
tests\core\test_core.py::test_check_options FAILED [ 33%]
tests\core\test_core.py::test_type_checking FAILED [ 50%]
tests\core\test_parser.py::test_parse_expr PASSED [ 66%]
tests\core\test_parser.py::test_parse_num PASSED [ 83%]
tests\core\test_parser.py::test_parse_add PASSED [100%]要查看哪些测试没有运行它们,请使用--collect-only标志:
λ pytest tests/core --collect-only
======================== test session starts ========================
...
collected 6 items
<Module 'tests/core/test_core.py'>
<Function 'test_regex_matching'>
<Function 'test_check_options'>
<Function 'test_type_checking'>
<Module 'tests/core/test_parser.py'>
<Function 'test_parse_expr'>
<Function 'test_parse_num'>
<Function 'test_parse_add'>
=================== no tests ran in 0.01 seconds ====================如果你想执行一个特定的测试,但记不起它的确切名称,那么--collect-only特别有用。
正如您可能已经注意到的,pytest 使用内置的assert语句在测试期间检查假设。与其他框架不同,您不需要记住各种self.assert*或self.expect*函数。虽然一开始这似乎不是什么大不了的事,但在花了一些时间使用普通断言之后,您将意识到这在多大程度上使编写测试变得更加有趣和自然。
同样,这里有一个失败的例子:
________________________ test_default_health ________________________
def test_default_health():
health = get_default_health('warrior')
> assert health == 95
E assert 80 == 95
tests\test_assert_demo.py:25: AssertionErrorPytest 显示了故障行,以及故障中涉及的变量和表达式。就其本身而言,这已经相当酷了,但 pytest 更进一步,提供了涉及其他数据类型的故障的专门解释。
在显示短字符串的解释时,pytest 使用一种简单的差异方法:
_____________________ test_default_player_class _____________________
def test_default_player_class():
x = get_default_player_class()
> assert x == 'sorcerer'
E AssertionError: assert 'warrior' == 'sorcerer'
E - warrior
E + sorcerer更长的字符串显示更智能的增量,使用difflib.ndiff快速发现差异:
__________________ test_warrior_short_description ___________________
def test_warrior_short_description():
desc = get_short_class_description('warrior')
> assert desc == 'A battle-hardened veteran, can equip heavy armor and weapons.'
E AssertionError: assert 'A battle-har... and weapons.' == 'A battle-hard... and weapons.'
E - A battle-hardened veteran, favors heavy armor and weapons.
E ? ^ ^^^^
E + A battle-hardened veteran, can equip heavy armor and weapons.
E ? ^ ^^^^^^^多行字符串也经过特殊处理:
def test_warrior_long_description():
desc = get_long_class_description('warrior')
> assert desc == textwrap.dedent('''\
A seasoned veteran of many battles. Strength and Dexterity
allow to yield heavy armor and weapons, as well as carry
more equipment. Weak in magic.
''')
E AssertionError: assert 'A seasoned v... \n' == 'A seasoned ve... \n'
E - A seasoned veteran of many battles. High Strength and Dexterity
E ? -----
E + A seasoned veteran of many battles. Strength and Dexterity
E allow to yield heavy armor and weapons, as well as carry
E - more equipment while keeping a light roll. Weak in magic.
E ? ---------------------------
E + more equipment. Weak in magic. 默认情况下,列表的断言失败也只显示不同的项:
____________________ test_get_starting_equiment _____________________
def test_get_starting_equiment():
expected = ['long sword', 'shield']
> assert get_starting_equipment('warrior') == expected
E AssertionError: assert ['long sword'...et', 'shield'] == ['long sword', 'shield']
E At index 1 diff: 'warrior set' != 'shield'
E Left contains more items, first extra item: 'shield'
E Use -v to get the full diff
tests\test_assert_demo.py:71: AssertionError请注意,pytest 显示了哪些索引不同,并且-v标志可用于显示列表之间的完全差异:
____________________ test_get_starting_equiment _____________________
def test_get_starting_equiment():
expected = ['long sword', 'shield']
> assert get_starting_equipment('warrior') == expected
E AssertionError: assert ['long sword'...et', 'shield'] == ['long sword', 'shield']
E At index 1 diff: 'warrior set' != 'shield'
E Left contains more items, first extra item: 'shield'
E Full diff:
E - ['long sword', 'warrior set', 'shield']
E ? ---------------
E + ['long sword', 'shield']
tests\test_assert_demo.py:71: AssertionError如果差异太大,pytest 足够聪明,只显示一部分,以避免显示太多输出,显示如下消息:
E ...Full output truncated (100 lines hidden), use '-vv' to show字典可能是 Python 中最常用的数据结构之一,因此,不出所料,pytest 为它们提供了专门的表示:
_______________________ test_starting_health ________________________
def test_starting_health():
expected = {'warrior': 85, 'sorcerer': 50}
> assert get_classes_starting_health() == expected
E AssertionError: assert {'knight': 95...'warrior': 85} == {'sorcerer': 50, 'warrior': 85}
E Omitting 1 identical items, use -vv to show
E Differing items:
E {'sorcerer': 55} != {'sorcerer': 50}
E Left contains more items:
E {'knight': 95}
E Use -v to get the full diff集合也有类似的输出:
________________________ test_player_classes ________________________
def test_player_classes():
> assert get_player_classes() == {'warrior', 'sorcerer'}
E AssertionError: assert {'knight', 's...r', 'warrior'} == {'sorcerer', 'warrior'}
E Extra items in the left set:
E 'knight'
E Use -v to get the full diff与列表一样,还有用于显示更详细输出的-v和-vv选项。
默认情况下,Python 的 assert 语句在失败时不提供任何细节,但正如我们刚才看到的,pytest 显示了关于失败断言中涉及的变量和表达式的大量信息。那么 pytest 是如何做到的呢?
Pytest 能够提供有用的异常,因为它实现了一种称为断言重写的机制。
断言重写的工作原理是安装一个截取标准 Python 导入机制的自定义导入挂钩。当 pytest 检测到一个测试文件(或插件)即将导入时,它不会加载模块,而是首先使用内置的ast模块将源代码编译成抽象语法树(AST)。然后,它搜索任何assert语句并重写语句,以便保留表达式中使用的变量,以便在断言失败时可以使用这些变量显示更多有用的消息。最后,它将重写的pyc文件保存到磁盘以进行缓存。
这一切可能看起来很神奇,但这个过程实际上是简单的、确定性的,最重要的是,完全透明的。
If you want more details, refer to http://pybites.blogspot.com.br/2011/07/behind-scenes-of-pytests-new-assertion.html, written by the original developer of this feature, Benjamin Peterson. The pytest-ast-back-to-python plugin shows exactly what the AST of your test files looks like after the rewriting process. Refer to: https://github.com/tomviner/pytest-ast-back-to-python.
好的 API 文档将清楚地解释每个函数的用途、参数和返回值。伟大的 API 文档还清楚地解释了引发哪些异常以及何时引发。
因此,测试在适当的情况下引发异常与测试 API 的主要功能同样重要。确保异常包含适当且清晰的消息以帮助用户理解问题也很重要。
假设我们正在为一个游戏编写一个 API。这个 API 允许程序员编写mods,这是一个插件,可以改变游戏的几个方面,从新的纹理到新的故事情节和角色类型。
此 API 有一个函数,允许 mod writer 创建新角色,并且在某些情况下会引发异常:
def create_character(name: str, class_name: str) -> Character:
"""
Creates a new character and inserts it into the database.
:raise InvalidCharacterNameError:
if the character name is empty.
:raise InvalidClassNameError:
if the class name is invalid.
:return: the newly created Character.
"""
...Pytest 可以很容易地检查您的代码是否使用raises语句引发了适当的异常:
def test_empty_name():
with pytest.raises(InvalidCharacterNameError):
create_character(name='', class_name='warrior')
def test_invalid_class_name():
with pytest.raises(InvalidClassNameError):
create_character(name='Solaire', class_name='mage')pytest.raises是 with 语句,确保传递给它的异常类将在其执行块内引发。欲了解更多详情(https://docs.python.org/3/reference/compound_stmts.html#the-附声明。让我们看看create_character是如何实现这些检查的:
def create_character(name: str, class_name: str) -> Character:
"""
Creates a new character and inserts it into the database.
...
"""
if not name:
raise InvalidCharacterNameError('character name empty')
if class_name not in VALID_CLASSES:
msg = f'invalid class name: "{class_name}"'
raise InvalidCharacterNameError(msg)
...如果您正在密切关注,您可能注意到前面代码中的复制粘贴错误实际上应该为类名检查引发一个InvalidClassNameError。
正在执行此文件:
======================== test session starts ========================
...
collected 2 items
tests\test_checks.py .F [100%]
============================= FAILURES ==============================
______________________ test_invalid_class_name ______________________
def test_invalid_class_name():
with pytest.raises(InvalidCharacterNameError):
> create_character(name='Solaire', class_name='mage')
tests\test_checks.py:51:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
name = 'Solaire', class_name = 'mage'
def create_character(name: str, class_name: str) -> Character:
"""
Creates a new character and inserts it into the database.
:param name: the character name.
:param class_name: the character class name.
:raise InvalidCharacterNameError:
if the character name is empty.
:raise InvalidClassNameError:
if the class name is invalid.
:return: the newly created Character.
"""
if not name:
raise InvalidCharacterNameError('character name empty')
if class_name not in VALID_CLASSES:
msg = f'invalid class name: "{class_name}"'
> raise InvalidClassNameError(msg)
E test_checks.InvalidClassNameError: invalid class name: "mage"
tests\test_checks.py:40: InvalidClassNameError
================ 1 failed, 1 passed in 0.05 seconds =================test_empty_name如期通过。test_invalid_class_name引发了InvalidClassNameError,因此pytest.raises没有捕获异常,导致测试失败(与任何其他异常一样)。
如本节开头所述,API 应在其引发的异常中提供明确的消息。在前面的示例中,我们只验证代码是否引发了适当的异常类型,而不是实际的消息。
pytest.raises可以接收可选的match参数,该参数是将与异常消息匹配的正则表达式字符串,并检查异常类型。有关更多详细信息,请访问:https://docs.python.org/3/howto/regex.html 。我们可以利用这一点进一步改进我们的测试:
def test_empty_name():
with pytest.raises(InvalidCharacterNameError,
match='character name empty'):
create_character(name='', class_name='warrior')
def test_invalid_class_name():
with pytest.raises(InvalidClassNameError,
match='invalid class name: "mage"'):
create_character(name='Solaire', class_name='mage')易于理解的
API 也在不断发展。提供旧函数的新的更好的替代方案,删除参数,使用某个功能的旧方法演变为更好的方法,等等。
API 编写者必须在保持旧代码正常工作以避免破坏客户机和提供更好的工作方式之间取得平衡,同时保持自己的 API 代码的可维护性。因此,通常采用的解决方案是,当 API 客户机使用旧的行为时,开始发出warnings,希望他们将代码更新为新的结构。警告消息显示在当前使用没有错误以保证异常的情况下,只是碰巧有新的更好的方法来实现。通常,在此更新的宽限期内会显示警告消息,之后不再支持旧方法。
Python 提供的标准警告模块正是为了实现这一目的,因此很容易就 api 中即将发生的更改向开发人员发出警告。有关更多详细信息,请访问:https://docs.python.org/3/library/warnings.html 。它允许您从许多警告类中进行选择,例如:
UserWarning:用户警告(user这里指的是开发者,不是软件用户)DeprecationWarning:未来将删除的功能ResourcesWarning:与资源使用相关
(此列表并非详尽无遗。有关完整列表,请参阅警告文档。有关更多详细信息,请转至:https://docs.python.org/3/library/warnings.html )。
警告类帮助用户控制应显示哪些警告以及应抑制哪些警告。
例如,假设一个电脑游戏的 API 提供了这个方便的函数,可以根据玩家角色的类名获取其起始生命值:
def get_initial_hit_points(player_class: str) -> int:
...随着时间的推移,开发人员决定在下一版本中使用enum而不是类名。有关更多详细信息,请访问:https://docs.python.org/3/library/enum.html ,更适合表示一组有限的值:
class PlayerClass(Enum):
WARRIOR = 1
KNIGHT = 2
SORCERER = 3
CLERIC = 4但是突然改变这一点会破坏所有客户机,因此他们明智地决定在下一版本中支持两种形式:str和PlayerClass``enum。他们不想永远支持这一点,所以每当一个类作为str传递时,他们就会开始显示警告:
def get_initial_hit_points(player_class: Union[PlayerClass, str]) -> int:
if isinstance(player_class, str):
msg = 'Using player_class as str has been deprecated' \
'and will be removed in the future'
warnings.warn(DeprecationWarning(msg))
player_class = get_player_enum_from_string(player_class)
...与上一节中的pytest.raises相同,pytest.warns函数允许您测试 API 代码是否产生预期的警告:
def test_get_initial_hit_points_warning():
with pytest.warns(DeprecationWarning):
get_initial_hit_points('warrior')与pytest.raises一样,pytest.warns可以接收可选的match参数,这是一个正则表达式字符串。有关更多详细信息,请转到:https://docs.python.org/3/howto/regex.html ,将与异常消息匹配:
def test_get_initial_hit_points_warning():
with pytest.warns(DeprecationWarning,
match='.*str has been deprecated.*'):
get_initial_hit_points('warrior')比较浮点数可能很棘手。有关更多详细信息,请访问:https://docs.python.org/3/tutorial/floatingpoint.html 。在现实世界中我们认为相等的数字不是由计算机硬件表示的:
>>> 0.1 + 0.2 == 0.3
False在编写测试时,比较代码生成的结果与我们预期的浮点值是非常常见的。如上所示,简单的==比较通常是不够的。一种常见的方法是使用已知公差,并使用abs正确处理负数:
def test_simple_math():
assert abs(0.1 + 0.2) - 0.3 < 0.0001但除了丑陋和难以理解之外,有时很难想出一个在大多数情况下都能奏效的宽容。选择的公差0.0001可能适用于上述数字,但不适用于非常大或非常小的数字。根据所执行的计算,您需要为每一组输入数字找到一个合适的容差,这既繁琐又容易出错。
pytest.approx通过自动为表达式中涉及的值选择合适的公差来解决此问题,提供了一个非常好的引导语法:
def test_approx_simple():
assert 0.1 + 0.2 == approx(0.3)您可以将上述内容理解为assert that 0.1 + 0.2 equals approximately to 0.3。
但approx功能并没有就此停止;它可用于比较:
- 编号顺序:
def test_approx_list():
assert [0.1 + 1.2, 0.2 + 0.8] == approx([1.3, 1.0])- 字典
values(非键):
def test_approx_dict():
values = {'v1': 0.1 + 1.2, 'v2': 0.2 + 0.8}
assert values == approx(dict(v1=1.3, v2=1.0))numpy阵列:
def test_approx_numpy():
import numpy as np
values = np.array([0.1, 0.2]) + np.array([1.2, 0.8])
assert values == approx(np.array([1.3, 1.0]))当测试失败时,approx提供一条漂亮的错误消息,显示失败的值和使用的公差:
def test_approx_simple_fail():
> assert 0.1 + 0.2 == approx(0.35)
E assert (0.1 + 0.2) == 0.35 ± 3.5e-07
E + where 0.35 ± 3.5e-07 = approx(0.35)Pytest 需要导入代码和测试模块,如何组织它们取决于您。Pytest 支持两种常见的测试布局,我们将在下面讨论。
您可以通过在模块旁边创建一个tests文件夹,将您的测试模块与它们正在测试的代码放在一起:
setup.py
mylib/
tests/
__init__.py
test_core.py
test_utils.py
__init__.py
core.py
utils.py通过将测试放在测试代码附近,您可以获得以下优势:
- 在这个层次结构中添加新的测试和测试模块并使它们保持同步更容易
- 您的测试现在是包的一部分,因此可以在其他环境中部署和运行它们
这种方法的主要缺点是,有些人不喜欢额外模块的附加包大小,这些模块现在与代码的其余部分打包在一起,但这通常是最小的,不太重要。
另外一个好处是,您可以使用--pyargs选项使用模块导入路径指定测试。例如:
λ pytest --pyargs mylib.tests这将执行在mylib.tests下找到的所有测试模块。
You might consider using _tests for the test module names instead of _test. This makes the directory easier to find because the leading underscore usually makes them appear at the top of the folder hierarchy. Of course, feel free to use tests or any other name that you prefer; pytest doesn't care as long as the test modules themselves are named test_*.py or *_test.py.
上述方法的另一种替代方法是将测试组织在与主包不同的目录中:
setup.py
mylib/
__init__.py
core.py
utils.py
tests/
__init__.py
test_core.py
test_utils.py 有些人喜欢这种布局,因为:
- 它将库代码和测试代码分开
- 源程序包中不包含测试代码
上述方法的一个缺点是,一旦您有了更复杂的层次结构,您可能会希望在测试目录中保留相同的层次结构,这就更难维护和保持同步:
mylib/
__init__.py
core/
__init__.py
foundation.py
contrib/
__init__.py
text_plugin.py
tests/
__init__.py
core/
__init__.py
test_foundation.py
contrib/
__init__.py
test_text_plugin.pySo, which layout is the best? Both layouts have advantages and disadvantages. Pytest itself works perfectly well with either of them, so feel free to choose a layout that you are more comfortable with.
现在我们来看看命令行选项,这些选项将使您的日常工作更有效率。如本章开头所述,这不是所有命令行功能的完整列表;只有那些你会使用(和爱)最多的。
通常,您不记得要执行的测试的完整路径或名称。在其他时候,套件中的许多测试都遵循类似的模式,您希望执行所有测试,因为您只是重构了代码的敏感区域。
通过使用-k <EXPRESSION>标志(来自关键字表达式,您可以运行item id与给定表达式松散匹配的测试:
λ pytest -k "test_parse"这将执行其项目 ID 中包含字符串parse的所有测试。您还可以使用布尔运算符编写简单的 Python 表达式:
λ pytest -k "parse and not num"这将执行其项目 ID 中包含parse但不包含num的所有测试。
在进行大规模重构时,您可能事先不知道如何或哪些测试将受到影响。在这些情况下,您可能会尝试猜测哪些模块将受到影响,并开始为这些模块运行测试。但是,通常情况下,您会打破比最初估计的更多的测试,并在所有测试都意外失败时点击CTRL+C以快速停止测试会话
在这些情况下,您可以尝试使用--maxfail=N命令行标志,该标志在N失败或错误后自动停止测试会话,或者使用快捷方式-x,该快捷方式等于--maxfail=1。
λ pytest tests/core -x这允许您快速查看第一个失败的测试并处理失败。修复故障原因后,可以继续运行-x处理下一个问题。
如果你觉得这很精彩,你不想跳过下一节!
Pytest 始终记住以前会话中失败的测试,并且可以重用该信息,直接跳到以前失败的测试。如前一节所述,如果您在大规模重构后以增量方式修复测试套件,这将是一个好消息。
通过--lf标志(表示上次失败*:*可以运行之前失败的测试
λ pytest --lf tests/core
...
collected 6 items / 4 deselected
run-last-failure: rerun previous 2 failures当与-x(--maxfail=1一起使用时,这两个标志是重构天堂:
λ pytest -x --lf 这允许您开始执行完整的套件,然后在第一次测试失败时 pytest 停止。修复代码,然后再次执行相同的命令行。Pytest 从失败的测试开始,如果通过,则继续进行(或者如果您还没有修复代码,则再次停止)。然后,它将在下一次失败时停止。冲洗并重复,直到所有测试再次通过。
请记住,在重构过程中执行另一个测试子集并不重要;pytest 总是记住哪些测试失败,而不管执行的命令行是什么。
如果您曾经进行过大规模的重构,并且必须跟踪哪些测试失败,这样您就不会浪费时间反复运行测试套件,那么您肯定会感激这一生产力的提升。
最后,--ff标志类似于--lf,但它将重新排序您的测试,以便先运行之前的故障,然后运行通过或尚未运行的测试:
λ pytest -x --lf
======================== test session starts ========================
...
collected 6 items
run-last-failure: rerun previous 2 failures first有时,开发人员会错误地、甚至有意地留下print语句,以供以后调试时使用。一些应用程序也可能写入stdout或stderr作为其正常操作或日志记录的一部分。
所有这些输出将使理解测试套件显示变得更加困难。因此,默认情况下,pytest 自动捕获写入stdout和stderr的所有输出
考虑此函数来计算给定的文本的散列,该散列在其上有一些调试代码:
import hashlib
def commit_hash(contents):
size = len(contents)
print('content size', size)
hash_contents = str(size) + '\0' + contents
result = hashlib.sha1(hash_contents.encode('UTF-8')).hexdigest()
print(result)
return result[:8]我们有一个非常简单的测试:
def test_commit_hash():
contents = 'some text contents for commit'
assert commit_hash(contents) == '0cf85793'在执行此测试时,默认情况下,您不会看到print调用的输出:
λ pytest tests\test_digest.py
======================== test session starts ========================
...
tests\test_digest.py . [100%]
===================== 1 passed in 0.03 seconds ======================那很好很干净。
但是这些打印语句可以帮助您理解和调试代码,这就是为什么当测试失败时,pytest 将显示捕获的输出。
让我们更改散列文本的内容,而不是散列本身。现在,pytest 将在错误回溯后的单独部分中显示捕获的输出:
λ pytest tests\test_digest.py
======================== test session starts ========================
...
tests\test_digest.py F [100%]
============================= FAILURES ==============================
_________________________ test_commit_hash __________________________
def test_commit_hash():
contents = 'a new text emerges!'
> assert commit_hash(contents) == '0cf85793'
E AssertionError: assert '383aa486' == '0cf85793'
E - 383aa486
E + 0cf85793
tests\test_digest.py:15: AssertionError
----------------------- Captured stdout call ------------------------
content size 19
383aa48666ab84296a573d1f798fff3b0b176ae8
===================== 1 failed in 0.05 seconds ======================在本地运行测试时,在失败的测试上显示捕获的输出非常方便,在 CI 上运行测试时更是如此。
在本地运行测试时,您可能希望禁用输出捕获,以查看实时打印了哪些消息,或者捕获是否干扰了捕获代码的其他操作。
在这些情况下,只需将-s传递到 pytest 即可完全禁用捕获:
λ pytest tests\test_digest.py -s
======================== test session starts ========================
...
tests\test_digest.py content size 29
0cf857938e0b4a1b3fdd41d424ae97d0caeab166
.
===================== 1 passed in 0.02 seconds ======================Pytest 有两种捕获输出的方法。通过--capture命令行标志可以选择使用哪种方法:
--capture=fd:捕获文件描述符级别的输出,这意味着捕获写入文件描述符 1(stdout)和 2(stderr)的所有输出。这将捕获甚至来自 C 扩展的输出,这是默认设置。--capture=sys:在 Python 级别捕获直接写入sys.stdout和sys.stderr的输出,而不尝试捕获系统级文件描述符。
通常,您不需要更改此方法,但在少数情况下,根据您的代码正在执行的操作,更改捕获方法可能会很有用。
为完整起见,还有--capture=no,与-s相同。
Pytest 将显示对失败测试的完整回溯,正如测试框架所期望的那样。但是,默认情况下,它不会显示大多数 Python 程序员习惯的标准回溯;它显示了不同的回溯:
============================= FAILURES ==============================
_______________________ test_read_properties ________________________
def test_read_properties():
lines = DATA.strip().splitlines()
> grids = list(iter_grids_from_csv(lines))
tests\test_read_properties.py:32:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests\test_read_properties.py:27: in iter_grids_from_csv
yield parse_grid_data(fields)
tests\test_read_properties.py:21: in parse_grid_data
active_cells=convert_size(fields[2]),
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
s = 'NULL'
def convert_size(s):
> return int(s)
E ValueError: invalid literal for int() with base 10: 'NULL'
tests\test_read_properties.py:14: ValueError
===================== 1 failed in 0.05 seconds ======================此回溯仅显示回溯堆栈中所有帧的一行代码和文件位置,第一行和最后一行除外,其中还显示了部分代码(粗体)。
虽然一开始有些人可能会觉得奇怪,但一旦你习惯了,你就会意识到这会使找出错误的原因变得更简单。通过查看回溯开始和结束的周围代码,通常可以更好地理解错误。我建议您尝试在几周内习惯 pytest 提供的默认回溯;我相信你会喜欢它,永远不会回头。
但是,如果您不喜欢 pytest 的默认回溯,那么还有其他回溯模式,它们由--tb标志控制。默认值为--tb=auto,前面已经显示过。在接下来的部分中,让我们看一看其他模式的概述。
此模式将显示故障回溯的所有****帧的部分代码,使其非常详细:
============================= FAILURES ==============================
_______________________ t________
def test_read_properties():
lines = DATA.strip().splitlines()
> grids = list(iter_grids_from_csv(lines))
tests\test_read_properties.py:32:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
lines = ['Main Grid,48,44', '2nd Grid,24,21', '3rd Grid,24,null']
def iter_grids_from_csv(lines):
for fields in csv.reader(lines):
> yield parse_grid_data(fields)
tests\test_read_properties.py:27:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
fields = ['3rd Grid', '24', 'null']
def parse_grid_data(fields):
return GridData(
name=str(fields[0]),
total_cells=convert_size(fields[1]),
> active_cells=convert_size(fields[2]),
)
tests\test_read_properties.py:21:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
s = 'null'
def convert_size(s):
> return int(s)
E ValueError: invalid literal for int() with base 10: 'null'
tests\test_read_properties.py:14: ValueError
===================== 1 failed in 0.05 seconds ======================此模式将显示故障回溯所有帧中的一行代码,提供简短的输出:
============================= FAILURES ==============================
_______________________ test_read_properties ________________________
tests\test_read_properties.py:32: in test_read_properties
grids = list(iter_grids_from_csv(lines))
tests\test_read_properties.py:27: in iter_grids_from_csv
yield parse_grid_data(fields)
tests\test_read_properties.py:21: in parse_grid_data
active_cells=convert_size(fields[2]),
tests\test_read_properties.py:14: in convert_size
return int(s)
E ValueError: invalid literal for int() with base 10: 'null'
===================== 1 failed in 0.04 seconds ======================此模式将输出与 Python 通常用于报告异常的完全相同的回溯,并且受到纯粹主义者的喜爱:
_______________________ test_read_properties ________________________
Traceback (most recent call last):
File "X:\CH2\tests\test_read_properties.py", line 32, in test_read_properties
grids = list(iter_grids_from_csv(lines))
File "X:\CH2\tests\test_read_properties.py", line 27, in iter_grids_from_csv
yield parse_grid_data(fields)
File "X:\CH2\tests\test_read_properties.py", line 21, in parse_grid_data
active_cells=convert_size(fields[2]),
File "X:\CH2\tests\test_read_properties.py", line 14, in convert_size
return int(s)
ValueError: invalid literal for int() with base 10: 'null'
===================== 1 failed in 0.03 seconds ======================此模式将为每个失败的测试输出一行,仅显示异常消息和错误的文件位置:
============================= FAILURES ==============================
X:\CH2\tests\test_read_properties.py:14: ValueError: invalid literal for int() with base 10: 'null'如果您正在进行大规模的重构,并且除了大量的失败之外,计划在之后使用--lf -x标志进入重构天堂模式,那么此模式可能会很有用。
这根本不显示任何回溯或故障消息,因此,首先运行套件以了解有多少故障也很有用,这样您就可以开始使用--lf -x标志逐步修复测试:
tests\test_read_properties.py F [100%]
===================== 1 failed in 0.04 seconds ======================最后,虽然这不是回溯模式标志,但当使用--tb=auto、--tb=long和--tb=short模式时,--showlocals(或-l作为快捷方式)通过显示局部变量及其值的列表来增强回溯模式。
例如,--tb=auto和--showlocals的输出如下:
_______________________ test_read_properties ________________________
def test_read_properties():
lines = DATA.strip().splitlines()
> grids = list(iter_grids_from_csv(lines))
lines = ['Main Grid,48,44', '2nd Grid,24,21', '3rd Grid,24,null']
tests\test_read_properties.py:32:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
tests\test_read_properties.py:27: in iter_grids_from_csv
yield parse_grid_data(fields)
tests\test_read_properties.py:21: in parse_grid_data
active_cells=convert_size(fields[2]),
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
s = 'null'
def convert_size(s):
> return int(s)
E ValueError: invalid literal for int() with base 10: 'null'
s = 'null'
tests\test_read_properties.py:14: ValueError
===================== 1 failed in 0.05 seconds ======================请注意,这使得查看坏数据的来源变得更加容易:测试开始时从文件中读取的'3rd Grid,24,null'字符串。
无论是在本地运行测试还是在 CI 中运行测试,--showlocals都非常有用,是公司的最爱。但要小心,因为这可能会带来安全风险:局部变量可能会暴露密码和其他敏感信息,因此请确保使用安全连接传输回溯,并小心将其公开。
在项目开始时,您的测试套件通常非常快,只需几秒钟即可运行,而且使用寿命很长。但是随着项目规模的增长,他们的测试套件也在增长,测试的数量和运行时间都在增长。
拥有一个缓慢的测试套件会影响生产效率,特别是如果您一直遵循 TDD 并运行测试的话。出于这个原因,定期查看运行时间最长的测试并分析是否可以加快测试速度是有益的:可能您正在使用一个大数据集,而数据集要小得多(更快),或者您可能正在执行对实际测试不重要的冗余步骤
当这种情况发生时,你会爱上--durations=N旗。此标志提供N最长运行测试的摘要,或使用零查看所有测试的摘要:
λ pytest --durations=5
...
===================== slowest 5 test durations ======================
3.40s call CH2/tests/test_slow.py::test_corner_case
2.00s call CH2/tests/test_slow.py::test_parse_large_file
0.00s call CH2/tests/core/test_core.py::test_type_checking
0.00s teardown CH2/tests/core/test_parser.py::test_parse_expr
0.00s call CH2/tests/test_digest.py::test_commit_hash
================ 3 failed, 7 passed in 5.51 seconds =================当您开始搜索要加速的测试时,此输出提供了宝贵的信息。
虽然这面旗帜不是你每天都会用到的东西,因为似乎很多人都不知道它,但它值得一提。
Pytest 显示关于失败测试的丰富回溯信息。额外的信息非常有用,但实际的页脚对于确定哪些测试实际失败没有多大帮助:
...
________________________ test_type_checking _________________________
def test_type_checking():
> assert 0
E assert 0
tests\core\test_core.py:12: AssertionError
=============== 14 failed, 17 passed in 5.68 seconds ================可以传递-ra标志,以便在会话结束时生成一个包含所有失败测试全名的漂亮摘要:
...
________________________ test_type_checking _________________________
def test_type_checking():
> assert 0
E assert 0
tests\core\test_core.py:12: AssertionError
====================== short test summary info ======================
FAIL tests\test_assert_demo.py::test_approx_simple_fail
FAIL tests\test_assert_demo.py::test_approx_list_fail
FAIL tests\test_assert_demo.py::test_default_health
FAIL tests\test_assert_demo.py::test_default_player_class
FAIL tests\test_assert_demo.py::test_warrior_short_description
FAIL tests\test_assert_demo.py::test_warrior_long_description
FAIL tests\test_assert_demo.py::test_get_starting_equiment
FAIL tests\test_assert_demo.py::test_long_list
FAIL tests\test_assert_demo.py::test_starting_health
FAIL tests\test_assert_demo.py::test_player_classes
FAIL tests\test_checks.py::test_invalid_class_name
FAIL tests\test_read_properties.py::test_read_properties
FAIL tests\core\test_core.py::test_check_options
FAIL tests\core\test_core.py::test_type_checking
=============== 14 failed, 17 passed in 5.68 seconds ================当直接从命令行运行套件时,此标志特别有用,因为滚动终端以找出哪些测试失败可能会很烦人。
该标志实际上是-r,它接受多个单字符参数:
f(失败):assert失败e(错误):引发了意外异常s(跳过):跳过(我们将在下一章讨论)x(xfailed):预期失败,但确实失败(我们将在下一章中讨论)X(xpassed):预计会失败,但通过了(!)(我们将在下一章讨论)p(通过):测试通过P(输出通过):即使通过测试,也会显示捕获的输出(小心–这通常会产生大量输出)a:显示除P之外的所有内容;这是默认值,通常是最有用的。
该标志可以接收上述任何组合。因此,例如,如果您只对失败和错误感兴趣,您可以将-rfe传递给 pytest。
总的来说,我建议坚持使用-ra,不要想太多,这样你会获得最大的好处。
用户可以使用名为pytest.ini的配置文件自定义一些 pytest 行为。此文件通常放置在存储库的根目录下,并包含许多应用于该项目的所有测试运行的配置值。它被置于版本控制之下,并与代码的其余部分一起提交。
该格式遵循一个简单的 ini 样式格式,所有与 pytest 相关的选项都位于[pytest]部分。有关更多详细信息,请访问:https://docs.python.org/3/library/configparser.html 。
[pytest]此文件的位置还定义 pytest 所称的根目录(rootdir):如果存在,则包含配置文件的目录被视为根目录。
根目录用于以下内容:
- 创建测试节点 ID 的步骤
- 作为存储项目信息的稳定位置(通过 pytest 插件和功能)
如果没有配置文件,根目录将取决于从哪个目录执行 pytest 以及传递了哪些参数(可以在此处找到算法说明:https://docs.pytest.org/en/latest/customize.html#finding-rootdir。因此,除了最简单的项目外,建议在所有项目中都使用pytest.ini文件,即使是空的。
Always define a pytest.ini file, even if empty.
如果您使用的是tox,您可以在传统的tox.ini文件中放置一个[pytest]部分,它也可以正常工作。有关更多详细信息,请访问:https://tox.readthedocs.io/en/latest/ :
[tox]
envlist = py27,py36
...
[pytest]
# pytest options这有助于避免存储库根目录中的文件过多,但这实际上是一个偏好问题。
现在,我们来看看更常见的配置选项。在接下来的章节中,我们将介绍更多选项,包括新功能。
我们学习了一些非常有用的命令行选项。其中一些可能会成为个人的最爱,但必须一直键入它们会很烦人。
可以使用addopts配置选项始终向命令行添加一组选项:
[pytest]
addopts=--tb=native --maxfail=10 -v使用该配置,键入以下内容:
λ pytest tests/test_core.py与键入相同:
λ pytest --tb=native --max-fail=10 -v tests/test_core.py请注意,尽管名称不同,addopts实际上在命令行中键入的其他选项之前插入了选项**。这使得显式传入时可以覆盖addopts中的大多数选项**
例如,下面的代码现在将显示自动回溯,而不是pytest.ini中配置的本机回溯:
λ pytest --tb=auto tests/test_core.py默认情况下,pytest 使用以下启发式方法收集测试:
- 匹配
test_*.py和*_test.py的文件 - 在测试模块内部,匹配
test*的函数和匹配Test*的类 - 在测试类内部,匹配
test*的方法
此约定易于理解,适用于大多数项目,但可被以下配置选项覆盖:
python_files:用于收集测试模块的模式列表python_functions:用于收集测试函数和测试方法的模式列表python_classes:用于收集测试类的模式列表
以下是更改默认值的配置文件示例:
[pytest]
python_files = unittests_*.py
python_functions = check_*
python_classes = *TestSuiteThe recommendation is to only use these configuration options for legacy projects that follow a different convention, and stick with the defaults for new projects. Using the defaults is less work and avoids confusing other collaborators.
前面显示的--lf和--ff选项由一个名为cacheprovider的内部插件提供,该插件将数据保存在磁盘上的目录中,以便在将来的会话中访问。该目录默认位于.pytest_cache下的根目录。此目录不应提交到版本控制。
如果您想更改该目录的位置,可以使用cache_dir选项。此选项还会自动展开环境变量:
[pytest]
cache_dir=$TMP/pytest-cache默认情况下,pytest 将在命令行上给定的参数的所有子目录上递归。当递归到从不包含任何测试的目录中时,这可能会使测试集合花费比预期更多的时间,例如:
- 虚拟人
- 构建工件
- 文档
- 版本控制目录
默认情况下,pytest 会尝试智能化,不会在模式为.*、build、dist、CVS、_darcs、{arch}、*.egg、venv的文件夹中递归。它还试图通过查看激活脚本的已知位置来自动检测 VirtualNV。
norecursedirs选项可用于覆盖 pytest 永远不应递归到的模式名的默认列表:
[pytest]
norecursedirs = artifacts _build docs您也可以使用--collect-in-virtualenv标志跳过virtualenv检测。
一般来说,用户很少需要重写默认值,但是如果您发现自己在项目中一次又一次添加相同的目录,请考虑打开一个问题。欲了解更多详情(https://github.com/pytest-dev/pytest/issues/new )。
如前所述,常见的目录结构是源布局之外的目录结构*,测试与tests或类似命名目录中的应用程序/库代码分离。在该布局中,使用testpaths配置选项非常有用:*
[pytest]
testpaths = tests当命令行中没有给出文件、目录或节点 ID 时,这将告诉 pytest 在何处查找测试,这可能会加快测试收集的速度。请注意,您可以配置多个由空格分隔的目录。
最后,一个鲜为人知的特性是,您可以使用-o/--override标志直接覆盖命令行中的任何配置选项。可以多次传递此标志以覆盖多个选项:
λ pytest -o python_classes=Suite -o cache_dir=$TMP/pytest-cache在本章中,我们介绍了如何使用virtualenv和pip安装 pytest。在那之后,我们开始研究如何编写测试,以及运行测试的不同方法,这样我们就可以只执行我们感兴趣的测试。我们概述了 pytest 如何为不同内置数据类型的失败测试提供丰富的输出信息。我们学习了如何使用pytest.raises和pytest.warns检查异常和警告,以及pytest.approx在比较浮点数时避免常见陷阱。然后,我们简要讨论了如何在项目中组织测试文件和模块。我们还研究了一些更有用的命令行选项,这样我们就可以立即提高效率。最后,我们介绍了pytest.ini文件如何用于持久命令行选项和其他配置。
在下一章中,我们将学习如何使用标记来帮助我们跳过某些平台上的测试,如何让我们的测试套件知道何时在我们的代码或外部库中修复了错误,以及如何对测试集进行分组,以便我们可以在命令行中有选择地执行它们。之后,我们将学习如何对不同的数据集应用相同的检查,以避免复制和粘贴测试代码。**