使用pip安装pytest
第三方包指的是独立于Python核心的库
更新pip
python -m pip install --upgrade pip
安装pytest
python -m pip install --user pytest
如果在执行这个命令时遇到麻烦, 可以尝试:
python -m pip install pytest
以上安装及更新均在cmd中输入完成
单元测试和测试用例
一种最简单的测试是单元测试,用于合适函数的某个方面没有问题。
测试用例是一组单元测试,这些单元测试一道合适函数在各种情况下的行为都符合要求。良好的测试用例考虑到了函数可能收到的各种输入,包含针对所有这些情况的测试。
全覆盖测试用例包含一整套单元测试,涵盖了各种可能的函数使用方式。对于大型项目,要进行全覆盖测试可能很难。
可通过的测试
使用pytest进行测试,会让单元测试编写起来非常简单。我们将编写一个测试函数,它会调用要测试的函数,并做出有关返回值的断言。如果断言正确,表示测试通过;如果断言不正确,表示测试未通过。
name_function.py
def get_formatted_name(first, last): """生成规范的名字""" full_name = f"{first} {last}" return full_name.title()
test_name_function.py
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_打头的文件,并运行其中的所有测试。
测试函数必须以test_打头,在测试过程中,pytest将找出并运行所有以test_打头的函数,此外,测试函数的名称应该比典型的函数名更长,更具描述性。
assert表示断言,断言就是声称满足特定的条件
运行测试
如果直接运行文件test_name_function.py,将不会有任何输出,因为我们没有调用这个测试函数。相反,应该让pytest替我们运行这个测试文件。
为此,打开一个终端窗口,pycharm为Alt+F12,在终端窗口执行命令pytest
tip:如果不知道如何在终端窗口中切换到正确的文件夹,请参阅1.5节。另外,如果出现一条消息,提示没有找到命令pytest,请执行命令python -m pytest
未通过的测试
测试未通过是什么样子呢,我们对get_formatted_name()进行修改
def get_formatted_name(first, middle, last): """生成规范的名字""" full_name = f"{first} {middle} {last}" return full_name.title()
运行原有的测试文件
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'
发现测试未通过,可以从报错信息中看出程序错误的地方。
在测试未通过时怎么办
在测试未通过时,如果检查的条件没错,那么测试通过意味着函数的行为是对的,而测试未通过意味着你编写的新代码有错。因此,在测试未通过时,不要修改测试。应为如果你这样做,即使能让测试通过,像测试那样调用函数的代码也将突然崩溃,相反,应该修复导致测试不能通过的代码:检查刚刚对函数所做的修改,找出这些修改是如何导致函数行为不符合预期的。
上面的程序我们就可以对其进行修改:
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()
添加新测试
def test_first_middle_last_name(): """能够正确处理像Wolfgang Amadeus Mozart""" formatted_name = get_formatted_name('wolfgang', 'mozart', 'amadeus') assert formatted_name == 'Wolfgang Amadeus Mozart'
测试结果显示两个测试都通过了
练习:
练习11.1
city_functions
def get_city_country(city, country): city_country = f"{city} {country}" return city_country.title()
test_cities
from city_functions import get_city_country def test_city_country(): """测试函数能都得到Santiago Chile的结果""" city_country = get_city_country('santiago', 'chile') assert city_country == 'Santiago Chile'
练习11.2
def get_city_country(city, country, population=None): if population: city_country = f"{city} {country} population:{population}" else: city_country = f"{city} {country}" return city_country.title()
from city_functions import get_city_country def test_city_country(): """测试函数能否得到Santiago Chile的结果""" city_country = get_city_country('santiago', 'chile') assert city_country == 'Santiago Chile' def test_city_country_population(): """测试函数能否得到Santiago Chile Population:3000的值""" city_country_population = get_city_country('santiago', 'chile', 3000)
测试类
很多程序会用到类,因此证明类能够正确地工作十分必要
各种断言
上面我们只用了一种断言,声称一个字符串变量取预期的值,在编写测试时我们科做出任何可表示为条件语句的断言。
assert a ==b 断言两个值相等
assert element (not) in list 断言元素在或不在列表中
assert not a 断言a的布尔求值为True
一个要测试的类
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(response)
为了证明AnonymousSurvey类能够正常工作,编写一个使用它的程序
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) # 显示调查结果 language_survey.show_results()
测试AnonymousSurvey类
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('Chinese') assert 'Chinese' in language_survey.responses
下面来核实,当用户提供三个答案时,它们都将被妥善存储
def test_store_three_response(): """测试三个答案会被妥善存储""" question = "What language did you first learn to speak?" language_survey = AnonymousSurvey(question) responses = ['English', 'Chinese', 'japanse'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses
使用夹具
在前面的test_survey.py中,我们每个 测试函数都创建了一个AnonymousSurvey实例。虽然这对于简单的示例来说不是问题,但是在包含数十乃至数百个测试中是个大问题。
在测试中,夹具可帮助我们搭建测试环境,这通常意味着创建供多个测试使用的资源,在pytest中,要创建夹具,可编写一个使用装饰器@pytest.fixture装饰的函数。装饰器是放在函数定义前面的指令。在运行函数前,Python将该指令应用于函数,以修改函数代码的行为。
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('Chinese') assert 'Chinese' in language_survey.responses def test_store_three_response(language_survey): """测试三个答案会被妥善存储""" responses = ['English', 'Chinese', 'japanse'] for response in responses: language_survey.store_response(response) for response in responses: assert response in language_survey.responses
当测试函数的一个形参与参与了装饰器@pytest.fixture的函数(夹具)同名时,将自动运行夹具,并将夹具返回的值传递给测试函数。
在想要使用夹具时,可编写一个函数来生成供多个测试函数使用的资源,再对这个函数应用装饰器@pytest.fixture,并让使用该资源的每个测试函数都接受一个与函数同门的形参。这样,测试将更简洁,编写和维护起来也将更加容易。
练习
练习11.3
import pytest from employee import Employee @pytest.fixture def employee(): employee = Employee('xiong', 'jiajin', 300000) return employee def test_give_default_raise(employee): """测试能否正常增加默认5000的年薪""" employee.give_raise() assert employee.salary == 305000 def test_give_custom_raise(employee): employee.give_raise(20000) assert employee.salary == 320000