Python--测试代码

目录

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 使用夹具


在编写函数或类时,还可为其编写测试。通过测试,可确定代码⾯对 各种输⼊都能够按要求⼯作。

1、使用pip安装pytest

第三⽅包(third-party package)指的是独⽴于 Python 核 ⼼的库。

然⽽,很多包并未被纳⼊标准库,因此得以独⽴于 Python 语⾔本⾝的更新 计划。为很多重要的功能是使⽤第三⽅包实 现的。

1.1 更新pip

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

1.2 安装putest

将 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 的情况下再次执⾏它。

2、测试函数

要学习测试,必须有要测试的代码。下⾯是⼀个简单的函数,它接受名和 姓并返回格式规范的姓名:

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 提供了⼀ 种⾃动测试函数输出的⾼效⽅式。

2.1 单元测试和测试用例

测试⽤例(test case)是⼀组单元测试, 这些单元测试⼀道核实函数在各种情况下的⾏为都符合要求。良好的测试 ⽤例考虑到了函数可能收到的各种输⼊,包含针对所有这些情况的测试。

全覆盖(full coverage)测试⽤例包含⼀整套单元测试,涵盖了各种可能的 函数使⽤⽅式。

2.2 可通过的测试

使⽤ 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'。

2.3 运行测试

打开⼀个终端窗⼝,并切换到这个测试⽂件所在的⽂件夹。如果你

使⽤的是 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

2.4 未通过的测试

修改

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。在末尾的简 短⼩结中,再次列出了最重要的信息。

2.5 解决测试未通过

如果检查的条件没错,那么测试通过意味 着函数的⾏为是对的,⽽测试未通过意味着你编写的新代码有错。因此, 在测试未通过时,不要修改测试。因为如果你这样做,即便能让测试通 过,像测试那样调⽤函数的代码也将突然崩溃。相反,应修复导致测试不 能通过的代码:检查刚刚对函数所做的修改,找出这些修改是如何导致函 数⾏为不符合预期的。

在这个⽰例中,新增的中间名参数是必不可少 的。就这⾥⽽⾔, 最佳的选择是让中间名变为可选的。

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()

2.6 添加新测试

确定 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 
==========================

3、测试类

如果针 对类的测试通过了,你就能确信对类所做的改进没有意外地破坏其原有的 ⾏为。

3.1 各种断言

到⽬前为⽌,我们只介绍了⼀种断⾔:声称⼀个字符串变量取预期的值。 在编写测试时,可做出任何可表⽰为条件语句的断⾔。如果该条件确实成 ⽴,你对程序⾏为的假设就得到了确认,可以确信其中没有错误。测试中常⽤的断⾔语句

断⾔

⽤途

assert a == b

断⾔两个值相等

assert a != b

断⾔两个值不等

assert a

断⾔ a 的布尔求值为 True

assert not a

断⾔ a 的布尔求值为 False

assert element in list

断⾔元素在列表中

assert element not in list

断⾔元素不在列表中

这⾥列出的只是九⽜⼀⽑,测试能包含任意可⽤条件语句表⽰的断⾔。

3.2 一个测试的类

类的测试与函数的测试相似,所做的⼤部分⼯作是测试类中⽅法的⾏为。

这是⼀

个帮助管理匿名调查的类:

# 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()

3.3 测试AnonymousSurvey类

下⾯来编写⼀个测试,对 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 的另 ⼀项功能来提⾼效率。

3.4 使用夹具

在测试中,夹具(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 对象的代码⾏。

你可能感兴趣的:(Python,python,开发语言,vscode)