Pytest 基础知识

已完成

让我们开始使用 Pytest 进行测试。 如上一单元中所述,Pytest 高度可配置,可以处理复杂的测试套件,但开始编写测试不需要太多。 事实上,编写测试的框架越简单越好。

在本部分结束时,应拥有开始编写第一个测试并使用 Pytest 运行它们所需的一切内容。

约定

在深入了解编写测试之前,我们必须了解 Pytest 所依赖的一些测试约定。

Python 中没有关于测试文件、测试目录或常规测试布局的硬规则。 你可以通过了解这些规则使用自动测试发现和执行,而无需进行任何其他配置。

测试目录和测试文件

测试的主目录是 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,报告会增加详细信息量,并提供精细比较。 此报告不仅会检测并显示失败,还支持快速进行更改以修正问题。