目录
1、使用pip安装pytest
1.1 更新pip
1.2 安装putest
2、测试函数
2.1 单元测试和测试用例
2.2 可通过的测试
2.3 运行测试
2.4 未通过的测试
2.5 解决测试未通过
2.6 添加新测试
3、测试类
3.1 各种断言
3.2 一个测试的类
3.3 测试AnonymousSurvey类
3.4 使用夹具
在编写函数或类时,还可为其编写测试。通过测试,可确定代码⾯对 各种输⼊都能够按要求⼯作。
第三⽅包(third-party package)指的是独⽴于 Python 核 ⼼的库。
然⽽,很多包并未被纳⼊标准库,因此得以独⽴于 Python 语⾔本⾝的更新 计划。为很多重要的功能是使⽤第三⽅包实 现的。
Python 提供了⼀款名为 pip 的⼯具,可⽤来安装第三⽅包。因为 pip 帮我们
安装来⾃外部的包,所以更新频繁,以消除潜在的安全问题。有鉴于此,
我们先来更新 pip。
打开⼀个终端窗⼝,执⾏如下命令:
$ python -m pip install --upgrade pip
❶ Requirement already satisfied: pip in /.../python3.11/site-packages
(22.0.4)
--snip--
❷ Successfully installed pip-22.1.2
可使⽤下⾯的命令更新系统中安装的任何包:
$ python -m pip install --upgrade package_name
将 pip 升级到最新版本后,就可以安装 pytest 了:
$ python -m pip install --user pytest
Collecting pytest
--snip--
Successfully installed attrs-21.4.0 iniconfig-1.1.1 ...pytest-7.x.x
这⾥使⽤的核⼼命令也是 pip install,但指定的标志不是 --upgrade,⽽是 --user。这个标志让 Python 只为当前⽤户装指定的 包。
可使⽤下⾯的命令安装众多的第三⽅包:
$ python -m pip install --user package_name
注意:如果在执⾏这个命令时遇到⿇烦,可尝试在不指定标志 --user 的情况下再次执⾏它。
要学习测试,必须有要测试的代码。下⾯是⼀个简单的函数,它接受名和 姓并返回格式规范的姓名:
def get_formatted_name(first, last):
"""⽣成格式规范的姓名"""
full_name = f"{first} {last}"
return full_name.title()
我们编写⼀个使⽤这个 函数的程序。程序 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}.")
这个程序从 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
从上述输出可知,合并得到的姓名正确⽆误。
现在假设要修改 get_formatted_name(),使其还能够处理中间名。为此,可在每 次修get_formatted_name() 后都进⾏测试:运⾏程序 names.py,并 输⼊像 Janis Joplin 这样的姓名。不过这太烦琐了。所幸 pytest 提供了⼀ 种⾃动测试函数输出的⾼效⽅式。
测试⽤例(test case)是⼀组单元测试, 这些单元测试⼀道核实函数在各种情况下的⾏为都符合要求。良好的测试 ⽤例考虑到了函数可能收到的各种输⼊,包含针对所有这些情况的测试。
全覆盖(full coverage)测试⽤例包含⼀整套单元测试,涵盖了各种可能的 函数使⽤⽅式。
使⽤ pytest 进⾏测试,会让单元测试编写起来⾮常简单。我们将编写⼀ 个测试函数,它会调⽤要测试的函数,并做出有关返回值的断⾔。如果断 ⾔正确,表⽰测试通过;如果断⾔不正确,表⽰测试未通过。
这个针对 get_formatted_name() 函数的测试如下:
from name_function import get_formatted_name
❶ def test_first_last_name():
"""能够正确地处理像 Janis Joplin 这样的姓名吗?"""
❷ formatted_name = get_formatted_name('janis', 'joplin')
❸ assert formatted_name == 'Janis Joplin'
测试⽂件的名称很重要,必须以 test_打头。当你让 pytest 运⾏测试时,它将查找以 test_打头的⽂件,并 运⾏其中的所有测试。
在这个测试⽂件中,⾸先导⼊要测试的get_formatted_name() 函数。 然后,定义⼀个测试函数 test_first_last_name()(⻅❶)。
这个函 数名⽐以前使⽤的都⻓,原因有⼆。
第⼀,测试函数必须以 test_ 打头。在测试过程中,pytest 将找出并运⾏所有以 test_ 打头的函数。
第⼆, 测试函数的名称应该⽐典型的函数名更⻓,更具描述性。你⾃⼰不会调⽤ 测试函数,⽽是由 pytest 替你查找并运⾏它们。因此,测试函数的名称 应⾜够⻓,让你在测试报告中看到它们时,能清楚地知道它们测试的是哪 些⾏为
接下来,调⽤要测试的函数(⻅❷)。像运⾏ names.py 时⼀样,这⾥在调 ⽤ get_formatted_name() 函数时向它传递了实参 'janis' 和 'joplin'。将这个函数的返回值赋给变量 formatted_name。
最后,做出⼀个断⾔(⻅❸)。断⾔(assertion)就是声称满⾜特定的条 件:这⾥声称 formatted_name 的值为 'Janis Joplin'。
打开⼀个终端窗⼝,并切换到这个测试⽂件所在的⽂件夹。如果你
使⽤的是 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
==========================
⼀些有关运⾏测试的系统的 信息(⻅❶)。该测试是从哪个⽬录运⾏的(⻅❷)pytest 找到了⼀个测试(⻅❸),并 指出了运⾏的是哪个测试⽂件(⻅❹)
注意:如果出现⼀条消息,提⽰没有找到命令 pytest,请执⾏命 令 python -m pytest
修改
get_formatted_name(),使其能够处理中间名,但同时故意让这个函
数⽆法正确地处理像 Janis Joplin 这样只有名和姓的姓名。
下⾯是 get_formatted_name() 函数的新版本,它要求通过⼀个实参指
定中间名:
def get_formatted_name(first, middle, last):
"""⽣成格式规范的姓名"""
full_name = f"{first} {middle} {last}"
return full_name.title()
对其进⾏测试时,我
们发现它不再能正确地处理只有名和姓的姓名了。
这次运⾏ pytest 时,输出如下:
$ pytest
========================= test session starts
=========================
--snip--
❶ test_name_function.py F
[100%]
❷ ============================== FAILURES
===============================
❸ ________________________ test_first_last_name
_________________________
def test_first_last_name():
"""能够正确地处理像 Janis Joplin 这样的姓名吗?"""
❹ > 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
==========================
输出中有⼀个字⺟ F(⻅❶),表明有⼀个测试未通过。然后是 FAILURES 部分(⻅❷),这是关注的焦点,因为在运⾏测试时,通常应 该关注未通过的测试。接下来,指出未通过的测试函数是 test_first_last_name()(⻅❸)。右尖括号(⻅❹)指出了导致测 试未能通过的代码⾏。下⼀⾏中的 E(⻅❺)指出了导致测试未通过的具体 错误:缺少必不可少的位置实参 'last',导致 TypeError。在末尾的简 短⼩结中,再次列出了最重要的信息。
如果检查的条件没错,那么测试通过意味 着函数的⾏为是对的,⽽测试未通过意味着你编写的新代码有错。因此, 在测试未通过时,不要修改测试。因为如果你这样做,即便能让测试通 过,像测试那样调⽤函数的代码也将突然崩溃。相反,应修复导致测试不 能通过的代码:检查刚刚对函数所做的修改,找出这些修改是如何导致函 数⾏为不符合预期的。
在这个⽰例中,新增的中间名参数是必不可少 的。就这⾥⽽⾔, 最佳的选择是让中间名变为可选的。
def get_formatted_name(first, last, middle=''):
"""⽣成格式规范的姓名"""
if middle:
full_name = f"{first} {middle} {last}"
else:
full_name = f"{first} {last}"
return full_name.title()
确定 get_formatted_name() ⼜能正确地处理简单的姓名后,我们再编 写⼀个测试,⽤于测试包含中间名的姓名。为此,在⽂件 test_name_function.py 中添加⼀个测试函数:
from name_function import get_formatted_name
def test_first_last_name():
--snip--
def test_first_last_middle_name():
"""能够正确地处理像 Wolfgang Amadeus Mozart 这样的姓名吗?"""
❶ formatted_name = get_formatted_name(
'wolfgang', 'mozart', 'amadeus')
❷ assert formatted_name == 'Wolfgang Amadeus Mozart'
为测试 get_formatted_name() 函数,我们先使⽤名、姓和中间名调⽤ 它(⻅❶),再断⾔返回的姓名与预期的姓名(名、中间名和姓)⼀致 (⻅❷)。再次运⾏ pytest,两个测试都通过了:
$ pytest
========================= test session starts
=========================
--snip--
collected 2 items
❶ test_name_function.py ..
[100%]
========================== 2 passed in 0.01s
==========================
如果针 对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的 ⾏为。
到⽬前为⽌,我们只介绍了⼀种断⾔:声称⼀个字符串变量取预期的值。 在编写测试时,可做出任何可表⽰为条件语句的断⾔。如果该条件确实成 ⽴,你对程序⾏为的假设就得到了确认,可以确信其中没有错误。测试中常⽤的断⾔语句
断⾔ |
⽤途 |
assert a == b |
断⾔两个值相等 |
assert a != b |
断⾔两个值不等 |
assert a |
断⾔ a 的布尔求值为 True |
assert not a |
断⾔ a 的布尔求值为 False |
assert element in list |
断⾔元素在列表中 |
assert element not in list |
断⾔元素不在列表中 |
这⾥列出的只是九⽜⼀⽑,测试能包含任意可⽤条件语句表⽰的断⾔。
类的测试与函数的测试相似,所做的⼤部分⼯作是测试类中⽅法的⾏为。
这是⼀
个帮助管理匿名调查的类:
# survey.py
class AnonymousSurvey:
"""收集匿名调查问卷的答案"""
❶ def __init__(self, question):
"""存储⼀个问题,并为存储答案做准备"""
self.question = question
self.responses = []
❷ def show_question(self):
"""显⽰调查问卷"""
print(self.question)
❸ def store_response(self, new_response):
"""存储单份调查答卷"""
self.responses.append(new_response)
❹ def show_results(self):
"""显⽰收集到的所有答卷"""
print("Survey results:")
for response in self.responses:
print(f"- {response}")
这个类⾸先存储⼀个调查问题(⻅❶),并创建了⼀个空列表,⽤于存储 答案。这个类包含打印调查问题的⽅法(⻅❷),在答案列表中添加新答 案的⽅法(⻅❸),以及将存储在列表中的答案打印出来的⽅法(⻅❹)。 要创建这个类的实例,只需提供⼀个问题即可。编写⼀个使⽤它的程 序:
# language_survey.py
from survey import AnonymousSurvey
# 定义⼀个问题,并创建⼀个表⽰调查的 AnonymousSurvey 对象
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
# 显⽰问题并存储答案
language_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
response = input("Language: ")
if response == 'q':
break
language_survey.store_response(response)
# 显⽰调查结果
print("\nThank you to everyone who participated in the survey!")
language_survey.show_results()
下⾯来编写⼀个测试,对 AnonymousSurvey 类的⾏为的⼀个⽅⾯进⾏验 证。
# test_survey.py
from survey import AnonymousSurvey
❶ def test_store_single_response():
"""测试单个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
❷ language_survey = AnonymousSurvey(question)
language_survey.store_response('English')
❸ assert 'English' in language_survey.responses
⾸先,导⼊要测试的 AnonymousSurvey 类。第⼀个测试函数验证:调查
问题的单个答案被存储后,它会包含在调查结果列表中。对于这个测试函
数,⼀个不错的描述性名称是 test_store_single_response()(⻅
❶)。如果这个测试未通过,我们就能通过测试⼩结中的函数名得知,在 存储单个调查答案⽅⾯存在问题。 要测试类的⾏为,需要创建其实例。在❷处,使⽤问题"What language did you first learn to speak?" 创建⼀个名language_survey 的实例,然后使⽤ store_response() ⽅法存储单个答案 English。接下来,通过断⾔ English 在列表
language_survey.responses 中,核实这个答案被妥善地存储了(⻅ ❸)。
$ pytest test_survey.py
========================= test session starts =========================
--snip--
test_survey.py . [100%]
========================== 1 passed in 0.01s ==========================
。下⾯来核实,当 ⽤户提供三个答案时,它们都将被妥善地存储。为此,再添加⼀个测试函 数:
from survey import AnonymousSurvey
def test_store_single_response():
--snip--
def test_store_three_responses():
"""测试三个答案会被妥善地存储"""
question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
❶responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
language_survey.store_response(response)
❷for response in responses:
assert response in language_survey.responses
我们将这个新函数命名为 test_store_three_responses(),并像 test_store_single_response() ⼀样,在其中创建⼀个调查对象。 先定义⼀个包含三个不同答案的列表(⻅❶),再对其中的每个答案都调⽤ store_response()。存储这些答案后,使⽤⼀个循环来断⾔每个答案 都包含在 language_survey.responses 中(⻅❷)。 再次运⾏这个测试⽂件,两个测试(针对单个答案的测试和针对三个答案 的测试)都通过了:
$ pytest test_survey.py
========================= test session starts =========================
--snip--
test_survey.py .. [100%]
========================== 2 passed in 0.01s ==========================
前述做法的效果很好,但这些测试有重复的地⽅。下⾯使⽤ pytest 的另 ⼀项功能来提⾼效率。
在测试中,夹具(fixture)可帮助我们搭建测试环境。这通常意味着创建供 多个测试使⽤的资源。在 pytest 中,要创建夹具,可编写⼀个使⽤装饰 器 @pytest.fixture 装饰的函数。装饰器(decorator)是放在函数定义 前⾯的指令。在运⾏函数前,Python 将该指令应⽤于函数,以修改函数代 码的⾏为。下⾯使⽤夹具创建⼀个 AnonymousSurvey 实例,让 test_survey.py 中的两 个测试函数都可使⽤它:
import pytest
from survey import AnonymousSurvey
❶ @pytest.fixture
❷ def language_survey():
"""⼀个可供所有测试函数使⽤的 AnonymousSurvey 实例"""question = "What language did you first learn to speak?"
language_survey = AnonymousSurvey(question)
return language_survey
❸ def test_store_single_response(language_survey):
"""测试单个答案会被妥善地存储"""
❹ language_survey.store_response('English')
assert 'English' in language_survey.responses
❺ def test_store_three_responses(language_survey):
"""测试三个答案会被妥善地存储"""
responses = ['English', 'Spanish', 'Mandarin']
for response in responses:
❻ language_survey.store_response(response)
for response in responses:
assert response in language_survey.responses
现在需要导⼊ pytest,因为我们使⽤了其中定义的⼀个装饰器。我们将装 饰器@pytest.fixture(⻅❶)应⽤于新函数language_survey() (⻅❷)。这个函数创建并返回⼀个AnonymousSurvey 对象。 请注意,两个测试函数的定义都变了(⻅❸和❺):都有⼀个名为 anguage_survey 的形参。当测试函数的⼀个形参与应⽤了装饰器
@pytest.fixture 的函数(夹具)同名时,将⾃动运⾏夹具,并将夹具 返回的值传递给测试函数。在这个⽰例中,language_survey() 函数向 test_store_single_response() 和
test_store_three_responses() 提供了⼀个 language_survey 实 例。 两个测试函数都没有新增代码,⽽且都删除了两⾏代码(⻅❹和❻):定义 问题的代码⾏,以及创建 AnonymousSurvey 对象的代码⾏。