自动化测试框架pytest教程

快速入门

准备

image

  • 安装

推荐安装anaconda,默认带pytest

# pip3 install pytest
...
$ pytest -h # 查看帮助
...

第一个实例(通过)

def test_passing():
    assert (1, 2, 3) == (1, 2, 3)

函数test_passing()为测试函数,因为它以test_开头,并且在名字以test_开头的文件中。assert 语句决定测试是通过还是失败。assert 是 Python 内置的关键字,如果 assert 后面的表达式是假的将引发 AssertionError 异常。在测试中引发的任何未捕获的异常都会导致测试失败。尽管任何类型的未捕获的异常都会导致测试失败,但传统上我们坚持用assert的AssertionError来决定测试的通过/失败。

  • 执行
$ pytest test_pass.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 1 item

test_pass.py .                                                           [100%]

============================== 1 passed in 0.08s ==============================

pass_test.py后的点表示一个测试运行并通过。 如果你需要更多信息,您可以使用-v或--verbose

$ pytest test_pass.py -v
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collecting ... collected 1 item

test_pass.py::test_passing PASSED                                        [100%]

============================== 1 passed in 0.08s ==============================

在彩色终端,PASSED和底线是绿色的。

第二个实例(失败)

def test_failing():
    assert (1, 2, 3) == (3, 2, 1)
  • 执行
$ pytest test_fail.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 1 item

test_fail.py F                                                           [100%]

================================== FAILURES ===================================
________________________________ test_failing _________________________________

    def test_failing():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Use -v to get more diff

test_fail.py:2: AssertionError
=========================== short test summary info ===========================
FAILED test_fail.py::test_failing - assert (1, 2, 3) == (3, 2, 1)
============================== 1 failed in 0.14s ==============================

pytest准确地告诉我们失败:At index 0 diff: 1 != 3。如果你有一个彩色终端,大部分内容会以红色显示。这个额外的部分显示了测试失败的确切位置和相关代码,被称为回溯。

同样-v可以获取更多信息

$ pytest test_fail.py -v
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collecting ... collected 1 item

test_fail.py::test_failing FAILED                                        [100%]

================================== FAILURES ===================================
________________________________ test_failing _________________________________

    def test_failing():
>       assert (1, 2, 3) == (3, 2, 1)
E       assert (1, 2, 3) == (3, 2, 1)
E         At index 0 diff: 1 != 3
E         Full diff:
E         - (3, 2, 1)
E         ?  ^     ^
E         + (1, 2, 3)
E         ?  ^     ^

test_fail.py:2: AssertionError
=========================== short test summary info ===========================
FAILED test_fail.py::test_failing - assert (1, 2, 3) == (3, 2, 1)
============================== 1 failed in 0.15s ==============================

更多运行方式

要运行pytest,你可以选择指定文件和目录。如果你不指定任何文件或目录,pytest将在当前工作目录和子目录中寻找测试。它寻找以test_开头或以_test结尾的.py文件。在ch1目录下,如果运行没有命令的pytest,你将运行两个文件的测试。

$ pytest --tb=no
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 2 items

test_fail.py F                                                           [ 50%]
test_pass.py .                                                           [100%]

=========================== short test summary info ===========================
FAILED test_fail.py::test_failing - assert (1, 2, 3) == (3, 2, 1)
========================= 1 failed, 1 passed in 0.08s =========================

我还使用了 --tb=no 标志来关闭回溯,因为我们现在并不真正需要完整的输出。

我们也可以通过指定测试或列出目录名称来获得相同的测试集来运行。

$ pytest --tb=no test_pass.py test_fail.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 2 items

test_pass.py .                                                           [ 50%]
test_fail.py F                                                           [100%]

=========================== short test summary info ===========================
FAILED test_fail.py::test_failing - assert (1, 2, 3) == (3, 2, 1)
========================= 1 failed, 1 passed in 0.09s =========================

我们也可以通过在文件名中加入::test_name来指定在测试文件中运行的测试函数。

$ pytest -v ch1/test_pass.py::test_passing
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collecting ... collected 1 item

ch1/test_pass.py::test_passing PASSED                                    [100%]

============================== 1 passed in 0.08s ==============================

测试发现

pytest执行的部分,即pytest去寻找要运行的测试,被称为测试发现。pytest能够找到所有我们希望它运行的测试,因为我们根据pytest的命名规则给它们命名。

在没有参数的情况下,pytest会在你的当前目录和所有子目录中寻找测试文件,并运行它找到的测试代码。如果你给pytest文件名、目录名或列表,它就会在那里寻找,而不是在当前目录。在命令行中列出的每个目录都会被检查出测试代码,以及任何子目录。

下面是对命名规则的简要概述

  • 测试文件应该被命名为test_.py或_test.py。
  • 测试方法和函数应该被命名为test_
  • 测试类应该被命名为Test

如果你的测试以不同方式命名,有办法改变这些发现规则。我将在第8章,配置文件中介绍如何做到这一点。

测试结果

到目前为止,我们已经看到一个通过的测试和一个失败的测试。然而,通过和失败并不是唯一可能的结果。

下面是可能结果:

  • PASSED (.):测试成功。
  • FAILED (F):测试失败。
  • SKIPPED (s): 测试被跳过。 你可以使用@pytest.mark.skip()或 pytest.mark.skipif()修饰器告诉pytest跳过测试。
  • XFAIL (x):预期测试失败。@pytest.mark.xfail()
  • XPASS (X):测试被标记为xfail,但它运行并通过了。
  • ERROR (E):异常发生在夹具或钩子函数的执行过程中,而不是测试函数的执行过程中。

测试函数

简介

在本章中,你将学习如何在测试Python包的情况下编写测试函数。如果你用 pytest 来测试 Python 包以外的东西,本章的大部分内容仍然适用。

我们将为一个简单的任务跟踪命令行程序写测试,这个程序叫做 Cards。我们将研究如何在测试中使用 assert,测试如何处理意外的异常,以及如何测试预期的异常。

最终,我们会有很多的测试。因此,我们将研究如何将测试组织成类、模块和目录。

安装示例应用

我们编写的测试代码需要能够运行应用程序的代码。应用程序代码 "是我们正在验证的代码,它有很多名字。你可能会听到它被称为生产代码、应用程序、被测代码(CUT:code under test )、被测系统(SUT:system under test)、被测设备(DUT:device under test),我们将使用 "应用代码 "这个术语。

测试代码 "是我们为了测试应用代码而编写的代码。具有讽刺意味的是,"测试代码 "是相当明确的,除了 "测试代码 "之外,没有太多的名字。

在我们的例子中,Cards 项目就是应用程序代码。它是一个可安装的 Python 包,我们需要安装它以测试它。安装它也将允许我们在命令行上玩弄 Cards 项目。如果你要测试的代码不是可以安装的 Python 包,你就必须用其他方法让你的测试看到你的代码。(一些替代方法在第 12 章 测试脚本和应用程序中讨论。)

  • 安装
$ pip install cards_proj/
Processing d:\code\pytest_quick\cards_proj
  Installing build dependencies: started
  Installing build dependencies: finished with status 'done'
  Getting requirements to build wheel: started
  Getting requirements to build wheel: finished with status 'done'
  Preparing metadata (pyproject.toml): started
  Preparing metadata (pyproject.toml): finished with status 'done'
Collecting typer==0.3.2
  Downloading typer-0.3.2-py3-none-any.whl (21 kB)
Collecting tinydb==4.5.1
  Downloading tinydb-4.5.1-py3-none-any.whl (23 kB)
Collecting rich==10.7.0
  Downloading rich-10.7.0-py3-none-any.whl (209 kB)
     ------------------------------------ 209.6/209.6 kB 706.8 kB/s eta 0:00:00
Requirement already satisfied: colorama<0.5.0,>=0.4.0 in d:\programdata\anaconda3\lib\site-packages (from rich==10.7.0->cards==1.0.0) (0.4.5)
Collecting commonmark<0.10.0,>=0.9.0
  Downloading commonmark-0.9.1-py2.py3-none-any.whl (51 kB)
     ---------------------------------------- 51.1/51.1 kB 1.3 MB/s eta 0:00:00
Requirement already satisfied: pygments<3.0.0,>=2.6.0 in d:\programdata\anaconda3\lib\site-packages (from rich==10.7.0->cards==1.0.0) (2.11.2)
Collecting click<7.2.0,>=7.1.1
  Downloading click-7.1.2-py2.py3-none-any.whl (82 kB)
     ---------------------------------------- 82.8/82.8 kB 1.5 MB/s eta 0:00:00
Building wheels for collected packages: cards
  Building wheel for cards (pyproject.toml): started
  Building wheel for cards (pyproject.toml): finished with status 'done'
  Created wheel for cards: filename=cards-1.0.0-py3-none-any.whl size=5011 sha256=61c1601a2053682eeccbafcf120c2606604175b8730f7681ca5a0a521b83c6c5
  Stored in directory: D:\Temp\pip-ephem-wheel-cache-5y22fhi5\wheels\0a\19\39\ed9dee4c4704cf05c72119386093ac231b9fe1c12faf5da0cc
Successfully built cards
Installing collected packages: commonmark, tinydb, rich, click, typer, cards
  Attempting uninstall: click
    Found existing installation: click 8.0.4
    Uninstalling click-8.0.4:
      Successfully uninstalled click-8.0.4
ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
black 22.6.0 requires click>=8.0.0, but you have click 7.1.2 which is incompatible.
Successfully installed cards-1.0.0 click-7.1.2 commonmark-0.9.1 rich-10.7.0 tinydb-4.5.1 typer-0.3.2
  • 演示

$cards add "联系作者:钉钉、抖音或微信pythontesting" --owner Andrew

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    todo    Andrew   联系作者:钉钉、抖音或微信pythontesting



$cards add "抢火车票" --owner Bob

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    todo    Andrew   联系作者:钉钉、抖音或微信pythontesting
  2    todo    Bob      抢火车票



$cards update 2 --owner Ted

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    todo    Andrew   联系作者:钉钉、抖音或微信pythontesting
  2    todo    Ted      抢火车票



$cards start 1

$cards finish 1

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    done    Andrew   联系作者:钉钉、抖音或微信pythontesting
  2    todo    Ted      抢火车票



$cards add "testing" --owner Bob

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    done    Andrew   联系作者:钉钉、抖音或微信pythontesting
  2    todo    Ted      抢火车票
  3    todo    Bob      testing



$cards delete 3

$cards

  ID   state   owner    summary
 ───────────────────────────────────────────────────────────────
  1    done    Andrew   联系作者:钉钉、抖音或微信pythontesting
  2    todo    Ted      抢火车票

cards可以用添加、更新、开始、结束和删除等动作来操作,而运行cards会列出卡片。

很好。现在我们准备写一些测试了。

了解被测系统

Cards的源代码被分成三层。CLI、 API, 和 DB. CLI处理与用户的交互。CLI调用API,它处理应用程序的大部分逻辑。API调用DB层(数据库),用于保存和检索应用数据。我们将在《软件架构》中更多地了解卡片的结构。

有一个数据结构用来在ClI和API之间传递信息,这个数据类叫做Card。

@dataclass
class Card:
    summary: str = None
    owner: str = None
    state: str = "todo"
    id: int = field(default=None, compare=False)

    @classmethod
    def from_dict(cls, d):
        return Card(**d)
    def to_dict(self):
        return asdict(self)

Data class在 3.7 版本中被添加到 Python 中,但它们对一些人来说可能仍然很陌生。卡片结构有三个字符串字段:摘要、所有者和状态,以及一个整数字段:ID。摘要、所有者和id字段默认为无。状态字段默认为 "todo"。id字段也使用字段方法来利用compare=False,这应该是告诉代码,当比较两个Card对象是否相等时,不要使用id字段。我们一定会测试这一点,以及其他方面。为了方便和清晰起见,还增加了几个方法:from_dict和to_dict,因为Card(**d)或dataclasses.asdict()不是很容易读。

当面对新的数据结构时,快速测试往往是很有帮助的,这样你就可以了解数据结构是如何工作的。所以,让我们从一些测试开始,验证我们对这个东西应该如何工作的理解。

ch2/test_card.py

from cards import Card


def test_field_access():
    c = Card("something", "brian", "todo", 123)
    assert c.summary == "something"
    assert c.owner == "brian"
    assert c.state == "todo"
    assert c.id == 123


def test_defaults():
    c = Card()
    assert c.summary is None
    assert c.owner is None
    assert c.state == "todo"
    assert c.id is None


def test_equality():
    c1 = Card("something", "brian", "todo", 123)
    c2 = Card("something", "brian", "todo", 123)
    assert c1 == c2


def test_equality_with_diff_ids():
    c1 = Card("something", "brian", "todo", 123)
    c2 = Card("something", "brian", "todo", 4567)
    assert c1 == c2
def test_inequality():
    c1 = Card("something", "brian", "todo", 123)
    c2 = Card("completely different", "okken", "done", 123)
    assert c1 != c2


def test_from_dict():
    c1 = Card("something", "brian", "todo", 123)
    c2_dict = {
        "summary": "something",
        "owner": "brian",
        "state": "todo",
        "id": 123,
    }
    c2 = Card.from_dict(c2_dict)
    assert c1 == c2


def test_to_dict():
    c1 = Card("something", "brian", "todo", 123)
    c2 = c1.to_dict()
    c2_expected = {
        "summary": "something",
        "owner": "brian",
        "state": "todo",
        "id": 123,
    }
    assert c2 == c2_expected

  • 执行
$ pytest test_card.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 7 items

test_card.py .......                                                     [100%]

============================== 7 passed in 0.14s ==============================

这些测试的重点是检查对架构的理解,并可能为其他人甚至为未来的我记录这些知识。这种检查我自己的理解的用法,以及真正把测试当作玩弄应用程序代码的小游乐场,是超级强大的,我认为如果更多的人从这种心态开始,他们会更喜欢测试。

还要注意的是,所有这些测试都使用普通的断言语句。

断言assert

当你写测试函数时,普通的Python assert语句是你沟通测试失败的主要工具。这在pytest中的简单性是非常好的。这也是促使很多开发者使用pytest而不是其他框架的原因。

如果你使用过任何其他测试框架,你可能已经看到了各种断言辅助函数。例如,下面是unittest的一些断言形式和断言辅助函数的列表。

在pytest中,你可以对任何表达式使用assert 。如果该表达式转换为bool的话,会评估为False,测试会失败。

pytest包括 "断言重写 "的功能,它可以拦截断言调用,并将其替换为可以告诉你更多关于断言失败原因的内容。让我们通过查看一个断言的失败来看看这种重写有多大帮助。

ch2/test_card_fail.py

def test_equality_fail():
    c1 = Card("sit there", "brian")
    c2 = Card("do something", "okken")
    assert c1 == c2

这个测试会失败,但有趣的是追踪信息。

$ pytest test_card_fail.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 1 item

test_card_fail.py F                                                      [100%]

================================== FAILURES ===================================
_____________________________ test_equality_fail ______________________________

    def test_equality_fail():
        c1 = Card("sit there", "brian")
        c2 = Card("do something", "okken")
>       assert c1 == c2
E       AssertionError: assert Card(summary=...odo', id=None) == Card(summary=...odo', id=None)
E
E         Omitting 1 identical items, use -vv to show
E         Differing attributes:
E         ['summary', 'owner']
E
E         Drill down into differing attribute summary:
E           summary: 'sit there' != 'do something'...
E
E         ...Full output truncated (8 lines hidden), use '-vv' to show

test_card_fail.py:7: AssertionError
=========================== short test summary info ===========================
FAILED test_card_fail.py::test_equality_fail - AssertionError: assert Card(su...
============================== 1 failed in 0.22s ==============================

这是很大的信息量。对于每个失败的测试,失败的确切行被显示出来,并有一个 > 指向失败。E行显示了关于断言失败的额外信息,以帮助你找出出错的原因。

我故意在test_equality_fail()中放了两个不匹配,但在前面的代码中只显示了第一个。让我们按照错误信息中的建议,用-vv标志再试一下。

$ pytest -vv test_card_fail.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0 -- D:\ProgramData\Anaconda3\python.exe
cachedir: .pytest_cache
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collecting ... collected 1 item

test_card_fail.py::test_equality_fail FAILED                             [100%]

================================== FAILURES ===================================
_____________________________ test_equality_fail ______________________________

    def test_equality_fail():
        c1 = Card("sit there", "brian")
        c2 = Card("do something", "okken")
>       assert c1 == c2
E       AssertionError: assert Card(summary='sit there', owner='brian', state='todo', id=None) == Card(summary='do something', owner='okken', state='todo', id=None)
E
E         Matching attributes:
E         ['state']
E         Differing attributes:
E         ['summary', 'owner']
E
E         Drill down into differing attribute summary:
E           summary: 'sit there' != 'do something'
E           - do something
E           + sit there
E
E         Drill down into differing attribute owner:
E           owner: 'brian' != 'okken'
E           - okken
E           + brian

test_card_fail.py:7: AssertionError
=========================== short test summary info ===========================
FAILED test_card_fail.py::test_equality_fail - AssertionError: assert Card(su...
============================== 1 failed in 0.21s ==============================

pytest明确列出了哪些属性是匹配的,哪些是不匹配的,并强调了确切的不匹配。

在pytest.org网站上可以找到更多种类的断言语句,并有很好的跟踪调试信息。

断言失败是测试代码导致测试失败的主要方式。然而,这并不是唯一的方式。

通过pytest.fail()和异常失败用例

如果有任何未捕获的异常,测试就会失败。这可能发生在

  • 断言语句失败,这将引发AssertionError异常。
  • 测试代码调用pytest.fail(),这将引发一个异常,或任何其他异常被引发。

虽然任何异常都可以使测试失败,但我更喜欢使用assert。在极少数情况下,如果assert不合适,可以使用pytest.fail()。

下面是使用pytest的fail()函数来明确地使测试失败的例子。

ch2/test_alt_fail.py

$ cat test_alt_fail.py
import pytest
from cards import Card


def test_with_fail():
    c1 = Card("sit there", "brian")
    c2 = Card("do something", "okken")
    if c1 != c2:
        pytest.fail("they don't match")

  • 执行
$ pytest test_alt_fail.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 1 item

test_alt_fail.py F                                                       [100%]

================================== FAILURES ===================================
_______________________________ test_with_fail ________________________________

    def test_with_fail():
        c1 = Card("sit there", "brian")
        c2 = Card("do something", "okken")
        if c1 != c2:
>           pytest.fail("they don't match")
E           Failed: they don't match

test_alt_fail.py:9: Failed
=========================== short test summary info ===========================
FAILED test_alt_fail.py::test_with_fail - Failed: they don't match
============================== 1 failed in 0.21s ==============================

当调用pytest.fail()或直接引发异常时,我们不会得到pytest提供的奇妙的断言重写。然而,也有合理的时候使用pytest.fail(),比如在断言帮助器中。

编写断言助手函数

断言助手是用来包装复杂的断言检查的函数。举个例子,Cards数据类的设置是这样的:两张ID不同的卡片仍然会报告相等。如果我们想有更严格的检查,我们可以写一个叫assert_identical的辅助函数,像这样。

from cards import Card
import pytest


def assert_identical(c1: Card, c2: Card):
    __tracebackhide__ = True
    assert c1 == c2
    if c1.id != c2.id:
        pytest.fail(f"id's don't match. {c1.id} != {c2.id}")


def test_identical():
    c1 = Card("foo", id=123)
    c2 = Card("foo", id=123)
    assert_identical(c1, c2)


def test_identical_fail():
    c1 = Card("foo", id=123)
    c2 = Card("foo", id=456)
    assert_identical(c1, c2)

assert_identical函数设置__tracebackhide__ = True。这是可选的。其效果是,失败的测试不会在回溯中包括这个函数。然后,正常的 assert c1 == c2 被用来检查除 ID 之外的所有内容是否相等。

最后,检查ID,如果它们不相等,就用pytest.fail()来拒绝测试,并希望有一个有用的信息。

让我们看看运行时是什么样子的。

$ pytest test_helper.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 2 items

test_helper.py .F                                                        [100%]

================================== FAILURES ===================================
_____________________________ test_identical_fail _____________________________

    def test_identical_fail():
        c1 = Card("foo", id=123)
        c2 = Card("foo", id=456)
>       assert_identical(c1, c2)
E       Failed: id's don't match. 123 != 456

test_helper.py:21: Failed
=========================== short test summary info ===========================
FAILED test_helper.py::test_identical_fail - Failed: id's don't match. 123 !=...
========================= 1 failed, 1 passed in 0.21s =========================

如果我们没有加入 tracebackhide = True,assert_identical 代码就会被包含在跟踪回溯中,在这种情况下,不会增加任何清晰度。我也可以使用 assert c1.id == c2.id, "id's don't match." 达到同样的效果,但我想展示一个使用 pytest.fail() 的例子。

注意,断言重写只适用于conftest.py文件和测试文件。

测试预期的异常

我们已经研究了任何异常如何导致测试失败。但是如果你正在测试的一段代码应该引发一个异常呢?你如何测试呢?

你可以使用 pytest.raises() 来测试预期的异常。

卡片API有CardsDB类,需要一个路径参数。如果我们不传入路径会发生什么?让我们来试试。

import cards


def test_no_path_fail():
    cards.CardsDB()
  • 执行
$ pytest --tb=short test_experiment.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 1 item

test_experiment.py F                                                     [100%]

================================== FAILURES ===================================
______________________________ test_no_path_fail ______________________________
test_experiment.py:5: in test_no_path_fail
    cards.CardsDB()
E   TypeError: __init__() missing 1 required positional argument: 'db_path'
=========================== short test summary info ===========================
FAILED test_experiment.py::test_no_path_fail - TypeError: __init__() missing ...
============================== 1 failed in 0.20s ==============================

这里我使用了 --tb=short 较短的回溯格式,因为我们不需要看到完整的回溯来发现哪个异常被引发。

TypeError异常是合理的,因为错误发生在试图初始化自定义的CardsDB类型时。我们可以写一个测试来确保这个异常被抛出,像这样。

import pytest
import cards


def test_no_path_raises():
    with pytest.raises(TypeError):
        cards.CardsDB()


def test_raises_with_info():
    match_regex = "missing 1 .* positional argument"
    with pytest.raises(TypeError, match=match_regex):
        cards.CardsDB()


def test_raises_with_info_alt():
    with pytest.raises(TypeError) as exc_info:
        cards.CardsDB()
    expected = "missing 1 required positional argument"
    assert expected in str(exc_info.value)

  • 执行
$ pytest test_exceptions.py
============================= test session starts =============================
platform win32 -- Python 3.9.13, pytest-7.1.2, pluggy-1.0.0
rootdir: D:\code\pytest_quick, configfile: pytest.ini
plugins: allure-pytest-2.12.0, Faker-4.18.0, tep-0.8.2, anyio-3.5.0
collected 3 items

test_exceptions.py ...                                                   [100%]

============================== 3 passed in 0.15s ==============================

with pytest.raises(TypeError): 的下一个代码块中的任何内容都应该引发一个TypeError异常。如果没有产生异常,测试就会失败。如果测试引发了一个不同的异常,则失败。

我们刚刚在test_no_path_raises()中检查了异常的类型。我们还可以检查以确保消息是正确的,或者异常的任何其他方面,比如额外的参数。

匹配参数需要一个正则表达式并与异常信息相匹配。如果是一个自定义的异常,你也可以使用 exc_info 或任何其他的变量名来询问异常的额外参数。exc_info对象将是ExceptionInfo类型的。关于ExceptionInfo的完整参考,请参见pytest文档。

posted @ 2023-04-23 13:38  磁石空杯  阅读(44)  评论(0编辑  收藏  举报