测试函数
要了解测试,我们需要代码来测试。 这是一个简单的函数,它接受名字和姓氏,并返回一个格式整齐的全名:
def get_formatted_name(first, last, middle=''):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
python
get_formatted_name() 函数将名字和姓氏之间用一个空格组合起来完成一个全名,然后将全名大写并返回。 为了检查 get_formatted_name() 是否有效,让我们编写一个使用此函数的程序。 程序 names.py 允许用户输入名字和姓氏,并看到格式整齐的全名:
from name_function import get_formatted_name
print("Enter 'q' at any time to quit.")
while True:
first = input("\nPlease give me a first name: ")
if first == 'q':
break
last = input("Please give me a last name: ")
if last == 'q':
break
formatted_name = get_formatted_name(first, last)
print(f"\tNeatly formatted name: {formatted_name}.")
python
该程序从 name_function.py 中导入 get_formatted_name()。 用户可以输入一系列名字和姓氏并查看生成的格式化全名:
Enter 'q' at any time to quit.
Please give me a first name: janis
Please give me a last name: joplin
Neatly formatted name: Janis Joplin.
Please give me a first name: bob
Please give me a last name: dylan
Neatly formatted name: Bob Dylan.
Please give me a first name: q
bash
我们可以看到这里生成的名字是正确的。 但是假设我们要修改 get_formatted_name() 以便它也可以处理中间名。 当我们这样做时,我们要确保我们不会破坏函数处理只有名字和姓氏的名称的方式。 我们可以通过运行 names.py 并在每次修改 get_formatted_name() 时输入一个名称(如 Janis Joplin)来测试我们的代码,但这会变得乏味。 幸运的是,pytest 提供了一种自动测试函数输出的有效方法。 如果我们将 get_formatted_name() 的测试自动化,我们总是可以确信该函数在给定我们为其编写测试的名称类型时能够正常工作。
单元测试和测试用例
有多种测试软件的方法。 最简单的测试类型之一是单元测试。 单元测试验证函数行为的一个特定方面是否正确。 测试用例是单元测试的集合,它们共同证明函数在您期望它处理的所有情况下都按预期运行。
一个好的测试用例会考虑函数可能接收的所有可能类型的输入,并包括代表每种情况的测试。 全覆盖的测试用例包括全方位的单元测试,涵盖您可以使用函数的所有可能方式。 实现对大型项目的全面覆盖可能会让人望而生畏。 为代码的关键行为编写测试通常就足够了,然后只有在项目开始广泛使用时才以完全覆盖为目标。
可通过的测试
使用 pytest,编写您的第一个单元测试非常简单。 我们将编写一个测试函数。 测试函数将调用我们正在测试的函数,我们将对返回的值进行断言。 如果我们的断言是正确的,那么测试就会通过; 如果断言不正确,测试将失败。
这是函数 get_formatted_name() 的第一个测试:
from name_function import get_formatted_name
def test_first_last_name(): (1)
"""Do names like 'Janis Joplin' work?"""
formatted_name = get_formatted_name('janis', 'joplin') (2)
assert formatted_name == 'Janis Joplin' (3)
python
在我们运行测试之前,让我们仔细看看这个函数。 测试文件的名称很重要; 它必须以 test_ 开头。 当我们要求 pytest 运行我们编写的测试时,它会查找任何以 test_ 开头的文件,并运行它在该文件中找到的所有测试。
在测试文件中,我们首先导入我们要测试的函数:get_formatted_name()。 然后我们定义一个测试函数:在本例中,test_first_last_name() ❶。 这是一个比我们一直使用的更长的函数名,这是有充分理由的。 首先,测试函数需要以单词 test 开头,后跟一个下划线。 任何以 test_ 开头的函数都将被 pytest 发现,并将作为测试过程的一部分运行。
此外,测试名称应该比典型的函数名称更长且更具描述性。 您永远不会自己调用该函数; pytest 会找到函数并为你运行它。 测试函数名称应该足够长,如果您在测试报告中看到函数名称,您将很好地了解正在测试的是什么行为。
接下来,我们调用我们正在测试的功能❷。 在这里,我们使用参数“janis”和“joplin”调用 get_formatted_name(),就像我们在运行 names.py 时使用的那样。 我们将这个函数的返回值赋值给 formatted_name。
最后,我们做一个断言❸。 断言是关于条件的声明。 这里我们声明 formatted_name 的值应该是 'Janis Joplin'。
运行测试
如果你直接运行文件 test_name_function.py,你不会得到任何输出,因为我们从未调用过测试函数。 相反,我们会让 pytest 为我们运行测试文件。
为此,打开一个终端窗口并导航到包含测试文件的文件夹。 如果您使用的是 VS Code,则可以打开包含测试文件的文件夹并使用嵌入在编辑器窗口中的终端。 在终端窗口中,输入命令 pytest。 这是你应该看到的:
$ pytest
==================== test session starts =========================
❶ platform darwin -- Python 3.x.x, pytest-7.x.x, pluggy-1.x.x
❷ rootdir: /.../python_work/chapter_11
❸ collected 1 item
❹ test_name_function.py .
[100%]========================== 1 passed in 0.00s ===============
bash
让我们试着理解这个输出。 首先,我们看到一些关于测试运行系统的信息❶。 我正在 macOS 系统上对此进行测试,因此您可能会在这里看到一些不同的输出。 最重要的是,我们可以看到正在使用哪些版本的 Python、pytest 和其他包来运行测试。
接下来,我们从 ❷ 看到运行测试的目录:在我的例子中,python_work/chapter_11。 我们可以看到 pytest 找到了一个要运行的测试 ❸,我们可以看到正在运行的测试文件 ❹。 文件名后的单点告诉我们通过了一个测试,100% 清楚地表明所有测试都已运行。 一个大项目可能有数百或数千个测试,圆点和完成百分比指示器有助于监控测试运行的整体进度。
最后一行告诉我们一个测试通过了,运行测试用了不到 0.01 秒。
此输出表明函数 get_formatted_name() 将始终适用于具有名字和姓氏的名称,除非我们修改该函数。 当我们修改 get_formatted_name() 时,我们可以再次运行这个测试。 如果测试通过,我们知道该函数仍然适用于像 Janis Joplin 这样的名字。
如果您不确定如何导航到终端中的正确位置,请参阅第 11 页的“从终端运行 Python 程序”。此外,如果您看到一条消息指出找不到 pytest 命令,请使用命令 python - m pytest 代替。 |
未通过的测试
失败的测试是什么样子的? 让我们修改 get_formatted_name() 以便它可以处理中间名,但我们这样做的方式是打破只有名字和姓氏的名字的功能,比如 Janis Joplin。
这是 get_formatted_name() 的新版本,它需要一个中间名参数:
def get_formatted_name(first, last, middle=''):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
python
这个版本应该适用于有中间名的人,但是当我们测试它时,我们发现我们已经破坏了只有名字和姓氏的人的功能。
这一次,运行 pytest 给出以下输出:
$ pytest
=============== test session starts =========================
--snip--
❶ test_name_function.py F
[100%]
❷ ==================== FAILURES ===============================
❸ ______________ test_first_last_name _________________________
def test_first_last_name():
"""Do names like 'Janis Joplin' work?"""
❹ > formatted_name = get_formatted_name('janis', 'joplin')
❺ E TypeError: get_formatted_name() missing 1 required positional
argument: 'last'
test_name_function.py:5: TypeError
================ short test summary info =======================
FAILED test_name_function.py::test_first_last_name - TypeError:
get_formatted_name() missing 1 required positional argument: 'last'
=================== 1 failed in 0.04s ==========================
bash
这里有很多信息,因为当测试失败时您可能需要知道很多信息。 输出中的第一项注释是单个 F ❶,它告诉我们一个测试失败了。 然后我们看到一个关注失败的部分 ❷,因为失败的测试通常是测试运行中最需要关注的事情。 接下来,我们看到 test_first_last_name() 是失败的测试函数 ❸。 尖括号 ❹ 表示导致测试失败的代码行。 下一行 ❺ 中的 E 显示导致失败的实际错误:由于缺少必需的位置参数导致的 TypeError,最后。 最重要的信息在末尾的简短摘要中重复出现,因此当您运行许多测试时,您可以快速了解哪些测试失败以及失败的原因。
在测试未通过时怎么办
当测试失败时你会怎么做? 假设您正在检查正确的条件,通过测试意味着函数运行正确,而失败测试意味着您编写的新代码中存在错误。 所以当测试失败时,不要更改测试。 如果这样做,您的测试可能会通过,但是任何像测试一样调用您的函数的代码都会突然停止工作。 相反,修复导致测试失败的代码。 检查您刚刚对函数所做的更改,并找出这些更改如何破坏所需的行为。
在这种情况下,get_formatted_name() 过去只需要两个参数:名字和姓氏。 现在它需要名字、中间名和姓氏。 添加该强制中间名参数打破了 get_formatted_name() 的原始行为。 这里最好的选择是使中间名可选。 一旦我们这样做,我们对像 Janis Joplin 这样的名字的测试应该再次通过,我们也应该能够接受中间名。 让我们修改 get_formatted_name() 以便中间名是可选的,然后再次运行测试用例。 如果通过,我们将继续确保函数正确处理中间名。
为了使中间名可选,我们将参数 middle 移动到函数定义中参数列表的末尾,并为其指定一个空的默认值。 我们还添加了一个 if 测试,它根据是否提供了中间名来正确构建全名:
def get_formatted_name(first, last, middle=''):
"""Generate a neatly formatted full name."""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
python
在这个新版本的 get_formatted_name() 中,中间名是可选的。 如果将中间名传递给函数,则全名将包含名字、中间名和姓氏。 否则,全名将仅包含名字和姓氏。 现在该函数应该适用于两种名称。 要确定该函数是否仍然适用于像 Janis Joplin 这样的名字,让我们再次运行测试:
$ pytest
================= test session starts =========================
--snip--
test_name_function.py .
[100%]
================== 1 passed in 0.00s ==========================
bash
现在测试通过了。 这是理想的; 这意味着该函数再次适用于像 Janis Joplin 这样的名字,而无需我们手动测试该函数。 修复我们的功能更容易,因为失败的测试帮助我们确定新代码如何破坏现有行为。
添加新测试
现在我们知道 get_formatted_name() 再次适用于简单名称,让我们为包含中间名的人编写第二个测试。 我们通过向文件 test_name_function.py 添加另一个测试函数来实现:
def test_first_last_middle_name():
"""Do names like 'Wolfgang Amadeus Mozart' work?"""
formatted_name = get_formatted_name( (1)
'wolfgang', 'mozart', 'amadeus')
assert formatted_name == 'Wolfgang Amadeus Mozart' (2)
python
我们将这个新函数命名为 test_first_last_middle_name()。 函数名称必须以 test_ 开头,以便在我们运行 pytest 时该函数会自动运行。 我们命名该函数以明确我们正在测试 get_formatted_name() 的哪种行为。 因此,如果测试失败,我们会立即知道哪些名称会受到影响。
为了测试该函数,我们调用带有名字、姓氏和中间名的 get_formatted_name() ❶,然后我们断言 ❷ 返回的全名与我们期望的全名(名字、中间名和姓氏)匹配。 当我们再次运行 pytest 时,两个测试都通过了:
$ pytest
=================== test session starts =========================
--snip--
collected 2 items
❶ test_name_function.py ..
[100%]
==================== 2 passed in 0.01s ==========================
bash
两个点❶表示通过了两个测试,从最后一行输出也能看出来。 这很棒! 我们现在知道该函数仍然适用于像 Janis Joplin 这样的名字,我们可以确信它也适用于像 Wolfgang Amadeus Mozart 这样的名字。