第 2 章 编写测试函数
在上一章中,您启动并运行了 pytest,并了解了如何针对文件和目录运行它。 在本章中,您将学习如何在测试 Python 包的上下文中编写测试函数。 如果您使用 pytest 来测试 Python 包以外的其他内容,本章的大部分内容仍然适用。
我们将为一个名为 Cards 的简单任务跟踪命令行应用程序编写测试。 我们将了解如何在测试中使用断言、测试如何处理意外异常以及如何测试预期异常。
最终,我们会进行很多测试。 因此,我们将研究如何将测试组织到类、模块和目录中。
安装演示应用
我们编写的测试代码需要能够运行 应用程序代码。 应用程序代码 是我们正在测试验证的代码。 它有很多名字。 您可能听说过它被称为生产代码、应用程序、被测代码 (CUT)、被测系统 (SUT)、被测设备 (DUT) 等。 在本书中,如果需要区分代码和测试代码,我们将使用术语 应用程序代码。
测试代码 是我们为了测试应用程序代码而编写的代码。 讽刺的是,测试代码 相当明确,除了 测试代码 之外没有太多名称。
在我们的例子中,Cards 项目是应用程序代码。 它是一个可安装的 Python 包,我们需要安装它才能测试它。 安装它还可以让我们玩纸牌。 在命令行上进行项目。 如果您正在测试的代码不是可以安装的 Python 包,则您必须使用其他方法来让您的测试看到您的代码。(【第 12 章 “测试脚本和应用程序”】 中讨论了一些替代方案。)
如果您还没有这样做,您可以从本书的网页下载该项目的源代码副本。 下载代码并将其解压缩到计算机上的某个位置。 您使用起来很舒服并且以后可以轻松找到。 在本书的其余部分中,我将将此位置称为 /path/to/code
。 Cards 项目位于 /path/to/code/cards_project
,本章的测试位于 /path/to/code/ch2
。
您可以使用上一章中使用的相同虚拟环境,为每一章创建新环境,或为整本书创建一个环境。 让我们在 /path/to/code/
级别创建一个并使用它,直到我们需要使用不同的东西为止:
$ cd /path/to/code
$ python -m venv venv
$ source venv/bin/activate
现在,激活虚拟环境后,安装本地 cards_proj 应用程序。 ./cards_proj/
前面的 ./
告诉 pip 在本地目录中查找,而不是尝试从 PyPI 安装。
(venv)$ pip install ./cards_proj/
Processing ./cards_proj
...
Successfully built cards
Installing collected packages: cards
Successfully installed cards
当我们这样做时,让我们确保 pytest 也已安装:
(venv)$ pip install pytest
对于每个新的虚拟环境,我们必须安装我们需要的一切,包括 pytest。
对于本书的其余部分,即使我将在虚拟环境中工作,我也仅将 $
显示为命令提示符,而不是 (venv)$
,这仅仅是为了节省水平空间和视觉噪音。
我们运行 cards 且来玩玩牌吧:
$ cards add do something --owner Brian
$ cards add do something else
$ cards
ID state owner summary
────────────────────────────────────────
1 todo Brian do something
2 todo do something else
$ cards update 2 --owner Brian
$ cards
ID state owner summary
────────────────────────────────────────
1 todo Brian do something
2 todo Brian do something else
$ cards start 1
$ cards finish 1
$ cards start 2
$ cards
ID state owner summary
──────────────────────────────────────────
1 done Brian do something
2 in prog Brian do something else
$ cards delete 1
$ cards
ID state owner summary
──────────────────────────────────────────
2 in prog Brian do something else
这些示例表明,可以使用 add
、update
、start
、finish
和 delete
操作来操作待办事项或 cards,并且运行不带任何操作的卡片将列出卡片。
很好。现在我们准备编写一些测试了。
编写知识构建测试
Cards 源代码分为三层:CLI
、API
和 DB
。 CLI
处理与用户的交互。 CLI
调用 API
,API
处理应用程序的大部分逻辑。 API
调用 DB
层(数据库),用于保存和检索应用程序数据。 我们将在【考虑软件架构】中更多地了解 Cards 的结构。
有一个数据结构用于在 CLI
和 API
之间传递信息,一个称为 Card
的数据类(data class):
@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)
数据类是在 Python 3.7 版本中添加的,但对某些人来说可能仍然是新的。 Card
结构具有三个字符串字段:summary
、owner
和 state
,以及一个整数字段:id
。 摘要、所有者和 id
字段默认为 None
。state
字段默认为 “todo”
。 id
字段还使用 field
方法来利用 compare=False
,这应该告诉代码在比较两个 Card
对象是否相等时,不要使用 id
字段。 我们一定会测试这一点以及其他方面。 为了方便和清晰,添加了其他几个方法:From_dict
和 to_dict
,因为 Card(**d)
或 dataclasses.asdict()
不太容易阅读。
当面对新的数据结构时,编写一些快速测试通常会很有帮助,这样您就可以了解数据结构的工作原理。 那么,让我们从一些测试开始,验证我们对这个东西应该如何工作的理解:
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
进行快速测试运行:
$ cd /path/to/code/ch2
$ pytest test_card.py
==================== test session starts =====================
collected 7 items
test_card.py ....... [100%]
===================== 7 passed in 0.20s ======================
我们可以从一项测试开始。然而,我想演示我们可以如何快速、简洁地编写一堆测试。 这些测试旨在演示如何使用数据结构。 它们不是详尽的测试;而是详尽的测试。 他们不是在寻找极端情况或失败案例,也不是在寻找使数据结构崩溃的方法。 我还没有尝试过将乱码或负数作为 ID
或大字符串传递。 这不是这组测试的重点。
这些测试的目的是检查我对结构如何工作的理解,并可能为其他人甚至未来的我记录这些知识。 这是检查我自己的理解的用途,并且确实是使用测试的用途。 作为玩应用程序代码的小游乐场,是非常强大的,我认为如果更多的人以这种心态开始,他们会喜欢进行更多测试。
另请注意,所有这些测试都使用普通的旧断言语句。 接下来我们来看看它们。
使用断言语句
当您编写测试函数时,普通的 Python 断言语句是传达测试失败的主要工具。 pytest 中的简单性非常出色。 这促使许多开发人员使用 pytest 而不是其他框架。
如果您使用过任何其他测试框架,您可能已经见过各种 assert
辅助函数。 例如,以下是 unittest 中的一些 assert
形式和 assert
辅助函数的列表:
pytest | unittest |
---|---|
assert something |
assertTrue(something) |
assert not something |
assertFalse(something) |
assert a == b |
assertEqual(a,b) |
assert a != b |
assertNotEqual(a,b) |
assert a is None |
assertIsNone(a) |
assert a is not None |
assertNotNone(a) |
assert a <= b |
assertLessEqual(a,b) |
… |
… |
使用 pytest,您可以将 assert <expression>
与任何表达式一起使用。 如果表达式转换为 bool
后计算结果为 False
,则测试将失败。
pytest 包含一个名为 “断言重写” 的功能,它拦截 assert
调用并将其替换为可以告诉您更多关于断言失败原因的信息。 让我们通过查看断言失败来看看这种重写有多大帮助:
def test_equality_fail():
c1 = Card("sit there", "brian")
c2 = Card("do something", "okken")
assert c1 == c2
该测试将会失败,但有趣的是回溯(traceback)信息:
$ pytest test_card_fail.py
==================== test session starts =====================
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 (7 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(summary=...odo', id=None) == Card(summary=...odo', id=None)
===================== 1 failed in 0.37s ======================
这是很多信息。 对于每个失败的测试,都会显示确切的失败行并指向失败。 E 行显示有关断言失败的额外信息,以帮助您找出问题所在。
我故意在 test_equality_Fail()
中放置了两个不匹配项,但前面的代码中只显示了第一个。 让我们使用 -vv
标志再试一次,如错误消息中所建议的那样:
$ pytest -vv test_card_fail.py
==================== test session starts =====================
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(summary='sit there', owner='brian', state='todo', id=None) == Card(summary='do something', owner='okken', state='todo', id=None)
===================== 1 failed in 0.39s ======================
嗯,我觉得这太酷了。 pytest 具体列出了哪些属性匹配,哪些属性不匹配,并突出显示了确切的不匹配。
前面的例子只使用了相等断言; 在 pytest.org 网站上可以找到更多种类的断言语句以及令人敬畏的跟踪调试信息。
仅供参考,我们可以看看 Python 在断言失败时默认为我们提供了什么。 我们可以不从 pytest 运行测试,而是直接从 Python 运行测试,方法是在文件末尾添加 if__name__ =='__main_'
块并调用 test_equality_fail()
,如下所示:
if __name__ == "__main__":
test_equality_fail()
使用 if __name__=='__main__'
是从文件运行某些代码的快速方法,但如果导入则不允许运行该代码。 当导入模块时,Python 会用模块的名称填充 __name__
,即不带 .py
的文件名。 但是,如果使用 python file.py
运行该文件,Python 将使用字符串 "__main__"
填充 __name__
。
使用 Python 直接运行测试,我们得到:
$ python test_card_fail.py
Traceback (most recent call last):
File "/path/to/code/ch2/test_card_fail.py", line 12, in <module> test_equality_fail()
File "/path/to/code/ch2/test_card_fail.py", line 7, in test_equality_fail assert c1 == c2
AssertionError
这并不能告诉我们太多。 pytest 版本为我们提供了有关断言失败原因的更多信息。
断言失败是测试代码导致测试失败的主要方式。然而,这不是唯一的方法。
因 pytest.fail() 和异常而失败
如果有任何未捕获的异常,测试将失败。如果发生这种情况,
-
断言语句失败,这将引发
AssertionError
异常, -
测试代码调用
pytest.fail()
,这将引发异常,或者 -
引发任何其他异常。
虽然任何异常都可能导致测试失败,但我更喜欢使用断言。 在极少数情况下断言不适合,请使用 pytest.fail()
。
下面是使用 pytest 的 fail()
函数显式失败测试的示例:
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 =====================
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.38s ======================
当调用 pytest.fail()
或直接引发异常时,我们无法获得 pytest 提供的精彩断言重写。 但是,有合理的地方使用 pytest.fail()
,例如在断言助手函数中。
编写断言辅助函数
断言助手是一个用于包装复杂断言检查的函数。例如,Cards 数据类的设置使得具有不同 ID
的两张 card
仍将报告相等。 如果我们想要进行更严格的检查,我们可以编写一个名为 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 =====================
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 != 456
================ 1 failed, 1 passed in 0.46s =================
如果我们没有放入 __tracebackhide__= True
,assert_identical
代码将包含在回溯中,在这种情况下,不会增加任何清晰度。 我也可以使用断言 c1.id == c2.id
,"id’s don’t match."。 效果大致相同,但我想展示一个使用 pytest.fail()
的示例)。
请注意,断言重写仅适用于 conftest.py
文件和测试文件。 有关更多详细信息,请参阅 pytest
文档。
测试期望异常
我们研究了任何异常如何导致测试失败。 但是,如果您正在测试的一些代码应该引发异常怎么办? 你如何测试这一点?
您使用 pytest.raises()
来测试预期的异常。
作为示例,cards
API 具有需要路径参数的 CardsDB
类。 如果我们不传递路径会怎样? 我们来试试:
import cards
def test_no_path_fail():
cards.CardsDB()
$ pytest --tb=short test_experiment.py
===================== test session starts ======================
collected 1 item
test_experiment.py F [100%]
=========================== FAILURES ===========================
______________________ test_no_path_fail _______________________
test_experiment.py:4: in test_no_path_fail
c = 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: __i...
====================== 1 failed in 0.06s =======================
这里我使用了 --tb=short
较短的回溯(traceback)格式,因为我们不需要查看完整的回溯来找出引发了哪个异常。
TypeError
异常似乎是合理的,因为在尝试初始化自定义 CardsDB 类型时会发生错误。 我们可以编写一个测试来确保抛出此异常,如下所示:
import pytest
import cards
def test_no_path_raises():
with pytest.raises(TypeError):
cards.CardsDB()
with pytest.raises(TypeError):
语句表示下一个代码块中的任何内容都会引发 TypeError
异常。如果没有引发异常,测试失败。如果测试引发了其他异常,则测试也失败。
我们刚刚在 test_no_path_raises()
中检查了异常的类型。我们还可以检查消息是否正确,或异常的其他方面,如额外参数:
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)
match
参数接受正则表达式并将其与异常消息进行匹配。如果是自定义异常,您还可以使用 exc_info
或任何其他变量名称来询问异常的额外参数。 exc_info
对象的类型为 ExceptionInfo
。 有关完整的 ExceptionInfo
参考,请参阅 pytest 文档。
构建测试函数
我建议确保将断言放在测试函数的末尾。这是一个很常见的建议,因此它至少有两个名字:Arrange-Act-Assert 和 Given-When-Then。
Bill Wake 最初于 2001 年命名了 Arrange-Act-Assert 模式。Kent Beck 后来将这种做法推广为测试驱动开发(TDD)的一部分。行为驱动开发(BDD)使用的术语是 Given-When-Then,这是 Ivan Moore 提出的一种模式,由 Dan North 推广。
分阶段测试有很多好处。将测试的 "准备做某事"、"做某事" 和 "检查是否成功" 三部分明确分开。这样,测试开发人员就能将注意力集中在每个部分,并清楚测试的真正内容。
一种常见的反模式是 "Arrange-Assert-Act-Assert-ActAssert…" 模式,即在大量行动之后进行状态或行为检查,以验证工作流。这看起来很合理,直到测试失败。任何操作都有可能导致失败,因此测试并没有专注于测试一种行为。也可能是 "安排" 中的设置导致了失败。这种交错断言模式会导致测试难以调试和维护,因为后来的开发人员根本不知道测试的初衷是什么。坚持使用 Given-When-Then 或 Arrange-Act-Assert 可以使测试保持专注,并使测试更易于维护。
三阶段结构是我自己的测试功能和本书中的测试所坚持的结构。
让我们以第一个测试为例,应用这种结构:
from cards import Card
def test_to_dict():
# GIVEN a Card object with known contents
c1 = Card("something", "brian", "todo", 123)
# WHEN we call to_dict() on the object
c2 = c1.to_dict()
# THEN the result will be a dictionary with known content
c2_expected = {
"summary": "something",
"owner": "brian",
"state": "todo",
"id": 123,
}
assert c2 == c2_expected
-
给定(Given)/安排(Arrange) —— 起始状态。您可以在此处设置数据或环境,为操作做好准备。
-
何时(when)/行动(Act) —— 执行某些操作。这是测试的重点——我们试图确保行为正常。
-
然后(Then)/断言(Assert) —— 一些预期的结果或最终状态应该发生。测试结束时,我们确保操作产生了预期的行为。
我倾向于更自然地使用 "给定-何时-然后" 来思考测试。有些人则认为使用 "安排-执行-插入"(Arrange-Act-Assert)更自然。这两种想法都很好。这种结构有助于保持测试功能的条理性,并将重点放在测试一种行为上。这种结构还有助于您思考其他测试用例。专注于一种起始状态有助于您思考可能与测试同一行为相关的其他状态。同样,关注一个理想结果也有助于您思考其他可能的结果,如失败状态或错误条件,这些也应通过其他测试用例进行测试。
将测试与类分组
到目前为止,我们已经在文件系统目录下的测试模块中编写了测试函数。这种测试代码结构实际上非常有效,足以满足许多项目的需要。不过,pytest 也允许我们用类来分组测试。
让我们选取几个与 Card 相等相关的测试函数,将它们归入一个类:
class TestEquality:
def test_equality(self):
c1 = Card("something", "brian", "todo", 123)
c2 = Card("something", "brian", "todo", 123)
assert c1 == c2
def test_equality_with_diff_ids(self):
c1 = Card("something", "brian", "todo", 123)
c2 = Card("something", "brian", "todo", 4567)
assert c1 == c2
def test_inequality(self):
c1 = Card("something", "brian", "todo", 123)
c2 = Card("completely different", "okken", "done", 123)
assert c1 != c2
该代码看起来与之前几乎相同,除了一些额外的空格,并且每个方法都必须有一个初始 self
参数。
我们现在可以通过指定类来一起运行所有这些:
$ cd /path/to/code/ch2
$ pytest -v test_classes.py::TestEquality
============================= test session starts =============================
collecting ... collected 3 items
test_classes.py::TestEquality::test_equality PASSED [ 33%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
============================== 3 passed in 0.17s ==============================
我们仍然可以测试一个单一的方法:
$ pytest -v test_classes.py::TestEquality::test_equality
============================= test session starts =============================
collecting ... collected 1 item
test_classes.py::TestEquality::test_equality PASSED [100%]
============================== 1 passed in 0.15s ==============================
如果您熟悉面向对象编程 (OOP) 和 Python 的类继承,就可以利用测试类层次结构来继承辅助方法。如果您不熟悉 OOP 等,也不必担心。在本书中,以及我自己使用的几乎所有测试类中,我使用它们的目的仅仅是为了将测试分组,以便于一起运行。我建议在生产测试代码中,也尽量少用测试类,主要用于分组。花哨地使用测试类的继承性肯定会让人感到困惑,将来可能还会让你自己感到困惑。
运行测试的子集
在上一节中,我们使用测试类来运行测试的子集。 在调试时或者如果您想将测试限制到您当时正在处理的代码库的特定部分,仅运行一小批测试会很方便。
pytest 允许您以多种方式运行测试子集:
子集 | 语法 |
---|---|
单个测试方法 |
pytest path/test_module.py::TestClass::test_method |
在一个类中的所有测试 |
pytest path/test_module.py::TestClass |
单个测试函数 |
pytest path/test_module.py::test_function |
一个模块中的所有测试 |
pytest path/test_module.py |
一个目录中的所有测试 |
pytest path |
测试匹配名称模式 |
pytest -k pattern |
通过标记进行测试 |
请参考标记章节部分 |
到目前为止,我们已经使用了除模式和标记子集之外的所有内容。但无论如何,让我们看一下例子。
我们将从顶级代码目录开始,以便可以使用 ch2 在命令行示例中显示路径:
$ cd /path/to/code
运行单个测试方法、测试类或模块:
$ pytest ch2/test_classes.py::TestEquality::test_equality
$ pytest ch2/test_classes.py::TestEquality
$ pytest ch2/test_classes.py
运行单个测试函数或模块:
$ pytest ch2/test_card.py::test_defaults
$ pytest ch2/test_card.py
运行整个目录:
$ pytest ch2
我们将在 【第 73 页的第 6 章 “标记”】 中介绍标记,但我们在这里讨论 -k
。
-k
参数接受一个表达式,并告诉 pytest 运行包含与该表达式匹配的子字符串的测试。 子字符串可以是测试名称或测试类名称的一部分。 让我们看一下 -k
的实际使用。
我们知道我们可以在 TestEquality
类中运行测试:
$ pytest ch2/test_classes.py::TestEquality
我们还可以使用 -k
并仅指定测试类名称:
$ cd /path/to/code/ch2
$ pytest -v -k TestEquality
=========================== test session starts ===========================
collected 24 items / 21 deselected / 3 selected
test_classes.py::TestEquality::test_equality PASSED [ 33%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
==================== 3 passed, 21 deselected in 0.06s =====================
甚至只是名称的一部分:
$ pytest -v -k TestEq
=========================== test session starts ===========================
collected 24 items / 21 deselected / 3 selected
test_classes.py::TestEquality::test_equality PASSED [ 33%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 66%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
==================== 3 passed, 21 deselected in 0.06s =====================
让我们运行名称中带有 “equality” 的所有测试:
$ pytest -v --tb=no -k equality
=========================== test session starts ===========================
collected 24 items / 17 deselected / 7 selected
test_card.py::test_equality PASSED [ 14%]
test_card.py::test_equality_with_diff_ids PASSED [ 28%]
test_card.py::test_inequality PASSED [ 42%]
test_card_fail.py::test_equality_fail FAILED [ 57%]
test_classes.py::TestEquality::test_equality PASSED [ 71%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 85%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
=============== 1 failed, 6 passed, 17 deselected in 0.08s ================
结果其中之一就是我们的失败例子。我们可以通过扩展表达式来排除它:
$ pytest -v --tb=no -k "equality and not equality_fail"
=========================== test session starts ===========================
collected 24 items / 18 deselected / 6 selected
test_card.py::test_equality PASSED [ 16%]
test_card.py::test_equality_with_diff_ids PASSED [ 33%]
test_card.py::test_inequality PASSED [ 50%]
test_classes.py::TestEquality::test_equality PASSED [ 66%]
test_classes.py::TestEquality::test_equality_with_diff_ids PASSED [ 83%]
test_classes.py::TestEquality::test_inequality PASSED [100%]
==================== 6 passed, 18 deselected in 0.07s =====================
允许使用关键字 and
、not
、or
和括号来创建复杂的表达式。 以下是名称中包含 “dict” 或 “ids” 但不包含 “TestEquality” 类中的所有测试的测试运行:
$ pytest -v --tb=no -k "(dict or ids) and not TestEquality"
=========================== test session starts ===========================
collected 24 items / 18 deselected / 6 selected
test_card.py::test_equality_with_diff_ids PASSED [ 16%]
test_card.py::test_from_dict PASSED [ 33%]
test_card.py::test_to_dict PASSED [ 50%]
test_classes.py::test_from_dict PASSED [ 66%]
test_classes.py::test_to_dict PASSED [ 83%]
test_structure.py::test_to_dict PASSED [100%]
==================== 6 passed, 18 deselected in 0.08s =====================
关键字标志 -k
以及 and
、not
、or
为准确选择要运行的测试添加了相当大的灵活性。 这在调试故障或开发新测试时确实非常有用。
审查
我们在本章中介绍了很多内容,并且正在顺利测试 Cards 应用程序。
-
示例代码应下载到
/path/to/code
中。 -
通过以下步骤将 Cards 应用程序(和 pytest)安装到虚拟环境中:
-
cd /path/to/code
-
python -m venv venv --prompt cards
-
source venv/bin/activate (or venv\Scripts\activate.bat on Windows)
-
pip install ./cards_proj
-
pip install pytest
-
-
pytest 使用断言重写,这允许我们使用标准的 Python 断言表达式。
-
测试可能因断言失败、调用 fail() 或任何未捕获的异常而失败。
-
pytest.raises() 用于测试预期的异常。
-
构造测试的一个好方法称为 Given-When-Then 或 Arrange-Act-Assert。
-
类可用于对测试进行分组。
-
调试时运行小部分测试非常方便,pytest 允许您以多种方式运行小批量测试。
-
-vv 命令行标志在测试失败期间显示更多信息。