Pytest 基础知识
让我们开始使用 Pytest 进行测试。 如上一单元所述,Pytest 高度可配置,可处理复杂的测试套件。 但是,开始编写测试时不需要用到太多。 事实上,编写测试的框架越简单越好。
在本部分结束时,应拥有开始编写第一个测试并使用 Pytest 运行它们所需的一切内容。
约定
在深入了解编写测试之前,我们必须了解 Pytest 所依赖的一些测试约定。
在 Python 中没有关于测试文件、测试目录或常规测试布局的硬性规则。 你可以通过了解这些规则使用自动测试发现和执行,而无需进行任何其他配置。
测试目录和测试文件
测试的主目录是 tests 目录。 可以将此目录置于项目的根级别,但有人也经常把它与代码模块放在一起。
注意
在本模块中,我们默认使用项目根目录中的 tests。
让我们看看名为 jformat
的小型 Python 项目的根目录是怎样的:
.
├── README.md
├── jformat
│ ├── __init__.py
│ └── main.py
├── setup.py
└── tests
└── test_main.py
tests 目录位于具有一个测试文件的项目的根目录中。 在这种情况下,此测试文件称为 test_main.py。 此示例演示了两个关键约定:
- 使用 tests 目录来放置测试文件和嵌套的测试目录。
- 测试文件的前缀为 test。 此前缀指示文件包含测试代码。
注意
避免将 test
(单一形式)用作目录名称。 test
名称是一个 Python 模块,因此创建一个与之同名的目录将会替代它。 请始终改用复数形式的 tests
。
测试函数
使用 Pytest 的一个有力论据是,它允许编写测试函数。 与测试文件类似,测试函数必须带有 test_
前缀。 test_
前缀可确保 Pytest 收集并执行测试。
一个简单的测试函数如下所示:
def test_main():
assert "a string value" == "a string value"
注意
如果你熟悉使用 unittest
,则在测试函数中使用 assert
可能会令人惊喜。 稍后我们会更详细地介绍普通断言,但使用 Pytest,你将通过普通断言获得丰富的失败报告。
测试类和测试方法
与文件和函数的约定类似,测试类和测试方法使用以下约定:
- 测试类的前缀为
Test
- 测试方法的前缀为
test_
与 Python 的 unittest
库的核心区别在于无需继承。
以下示例对类和方法使用这些前缀和其他 Python 命名约定。 它演示了一个小型测试类,该类在检查应用程序中的用户名。
class TestUser:
def test_username(self):
assert default() == "default username"
运行测试
Pytest 既是测试框架,也是测试运行程序。 测试运行程序是命令行中的可执行文件,在高级别可以:
- 通过查找测试运行的所有测试文件、测试类和测试函数来执行测试集合。
- 通过执行所有测试来启动测试运行。
- 跟踪测试失败、测试错误和测试通过的信息。
- 在测试运行结束时提供丰富的报告。
注意
由于 Pytest 是外部库,因此必须安装后才能使用。
鉴于 test_main.py 文件中的这些内容,我们可以看到 Pytest 运行测试时的行为:
# contents of test_main.py file
def test_main():
assert True
在命令行中,在 test_main.py 文件所在的同一路径中,可以运行 pytest
可执行文件:
$ pytest
=========================== test session starts ============================
platform -- Python 3.10.1, pytest-6.2.5, py-1.11.0, pluggy-1.0.0
rootdir: /private/tmp/project
collected 1 item
test_main.py . [100%]
============================ 1 passed in 0.00s =============================
在后台,Pytest 可收集测试文件中的示例测试,而无需进行任何配置。
强大的断言语句
到目前为止,测试示例全部使用普通的 assert
调用。 在 Python 中,assert
语句不用于测试,因为它在断言失败时缺少正确的报告。 但是,Pytest 没有此限制。 在后台,Pytest 使语句能够执行丰富的比较,而无需强制用户编写更多代码或配置任何内容。
通过使用普通的 assert
语句,可以使用 Python 的运算符。 例如:>
、<
、!=
、>=
或 <=
。 所有 Python 运算符都是有效的。 此功能可能是 Pytest 最关键的功能:你不需要了解用于编写断言的新语法。
让我们看看在处理与 Python 对象的常见比较时它是如何转换的。 在本例中,我们在比较长字符串时完成失败报告:
================================= FAILURES =================================
____________________________ test_long_strings _____________________________
def test_long_strings():
left = "this is a very long strings to be compared with another long string"
right = "This is a very long string to be compared with another long string"
> assert left == right
E AssertionError: assert 'this is a ve...r long string' == 'This is a ve...r long string'
E - This is a very long string to be compared with another long string
E ? ^
E + this is a very long strings to be compared with another long string
E ? ^ +
test_main.py:4: AssertionError
Pytest 显示有关失败的有用上下文。 字符串开头的大小写不正确,单词中包含一个额外字符。 但除字符串外,Pytest 还可以帮助其他对象和数据结构。 例如,以下是它处理列表的方式:
________________________________ test_lists ________________________________
def test_lists():
left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
right = ["sugar", "coffee", "wheat", "salt", "water", "milk"]
> assert left == right
E AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'co...ater', 'milk']
E At index 1 diff: 'wheat' != 'coffee'
E Full diff:
E - ['sugar', 'coffee', 'wheat', 'salt', 'water', 'milk']
E ? ---------
E + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E ? +++++++++
test_main.py:9: AssertionError
此报告标识索引 1(列表中的第二项)有所不同。 它不仅标识索引号,还提供失败的表示形式。 除了提供项比较之外,它还可以报告项是否缺失,并提供信息,使你准确得知可能是哪一项。 在以下情况中,此项为 "milk"
:
________________________________ test_lists ________________________________
def test_lists():
left = ["sugar", "wheat", "coffee", "salt", "water", "milk"]
right = ["sugar", "wheat", "salt", "water", "milk"]
> assert left == right
E AssertionError: assert ['sugar', 'wh...ater', 'milk'] == ['sugar', 'wh...ater', 'milk']
E At index 2 diff: 'coffee' != 'salt'
E Left contains one more item: 'milk'
E Full diff:
E - ['sugar', 'wheat', 'salt', 'water', 'milk']
E + ['sugar', 'wheat', 'coffee', 'salt', 'water', 'milk']
E ? ++++++++++
test_main.py:9: AssertionError
最后,让我们看看它如何处理字典。 如果存在失败,则比较两个大字典可能会让人难以应对,但 Pytest 在提供上下文并查明失败时会执行未完成的工作:
____________________________ test_dictionaries _____________________________
def test_dictionaries():
left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
> assert left == right
E AssertionError: assert {'county': 'F...rry Ln.', ...} == {'county': 'F...ry Lane', ...}
E Omitting 3 identical items, use -vv to show
E Differing items:
E {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E {'number': 39} != {'number': 38}
E Full diff:
E {
E 'county': 'Frett',...
E
E ...Full output truncated (12 lines hidden), use '-vv' to show
在此测试中,字典中有两个失败。 一个是 "street"
值不同,另一个是 "number"
不匹配。
Pytest 可以准确地检测到这些差异(即使它是单个测试中的一个失败)。 由于字典包含许多项,Pytest 省略相同的部分,且只显示相关内容。 如果我们使用建议的 -vv
标记来使输出内容更加详细,让我们看看会发生什么情况:
____________________________ test_dictionaries _____________________________
def test_dictionaries():
left = {"street": "Ferry Ln.", "number": 39, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
right = {"street": "Ferry Lane", "number": 38, "state": "Nevada", "zipcode": 30877, "county": "Frett"}
> assert left == right
E AssertionError: assert {'county': 'Frett',\n 'number': 39,\n 'state': 'Nevada',\n 'street': 'Ferry Ln.',\n 'zipcode': 30877} == {'county': 'Frett',\n 'number': 38,\n 'state': 'Nevada',\n 'street': 'Ferry Lane',\n 'zipcode': 30877}
E Common items:
E {'county': 'Frett', 'state': 'Nevada', 'zipcode': 30877}
E Differing items:
E {'number': 39} != {'number': 38}
E {'street': 'Ferry Ln.'} != {'street': 'Ferry Lane'}
E Full diff:
E {
E 'county': 'Frett',
E - 'number': 38,
E ? ^
E + 'number': 39,
E ? ^
E 'state': 'Nevada',
E - 'street': 'Ferry Lane',
E ? - ^
E + 'street': 'Ferry Ln.',
E ? ^
E 'zipcode': 30877,
E }
通过运行 pytest -vv
,报告会增加详细信息量,并提供精细比较。 此报告不仅会检测并显示失败,还支持快速进行更改以修正问题。