接上篇的《Python编程从入门到实践》第三版 学习笔记 附加篇1(第10章节),今天更新第11章 测试代码,此篇属于附加篇2,主要介绍了代码的测试。为确保不出问题,软件发行或更新前要进行测试,本章学习如何针对函数和类创建测试函数用以测试程序能否按预期运行,最后介绍了夹具的使用用以避免类的测试中重复创建实例。pytest是第三方库(外部库),其包含一组工具,可以帮助你快速轻松编写测试,先在终端运行:pytest --version 确认pytest版本,看下自己电脑有没有安装pytest,没有安装的话终端运行pip install pytest进行安装。单元测试(unit test):用于核实函数的某个方面没有问题;测试用例(test case):是一组单元测试,用于核实在各种情况下函数都符合要求,考虑函数可能收到的各种输入;全覆盖(full coverage)测试:包含一整套单元测试,覆盖各种可能的函数使用注:通常最初只要针对代码重要行为编写测试即可,待广泛使用时在考虑全覆盖待测试name_function文件中的待测试函数get_formatted_name() :测试文件test_name_function.py中的测试函数test_first_last_name():②.测试函数的命名,需注意要以test_打头,名字可以长,需具备描述性④.使用assert(断言),声称一个字符串变量取得预期的值注:测试函数所在文件也要以test_开头(有一些特殊情况可以不用,但对于初学者暂不考虑)打开测试文件所在文件夹,在终端执行pytest,将自动运行测试文件:①.在测试文件文件夹下运行pytest;②.显示版本号;③.显示文件位置④.显示1个测试文件被找到;⑤显示测试文件名;⑥.显示测试通过数量和时间 | |
| |
| |
| |
| |
| |
| assert element not in list | |
简而言之断言就是代码里设置的检查点,如果检查通过程序继续运行,若检查不过,pytest会报错,并告诉你哪个断言和什么原因没有通过。注:该类中responses的内容一开始是空的,是一个待填入调查答案的空列表,__init__()默认方法内通常接收外部传入的实参,而responses作为一个带填入答案的列表一开始为空,因此不需要用户在创建实例时提供,只要在默认方法内部通过对该属性赋空列表即可解析:断言答案在responses列表中,用以判断答案是否已被正确存入。
注:如果在执行命令pytest时没有指定任何参数,pytest将运行它在当前目录中找到的所有测试。专注于一个测试文件可将该测试文件的名称作为参数传递给pytest:pytest test_survey.py
5.夹具(fixture):用于包含大量测试的项目中,使用夹具可以不用在每个测试函数中重复创建一个类的实例。编写一个使用装饰器@pytest.fixture '装饰'的函数,该函数中会新建一个实例,后面所有的测试函数的形参都会使用该函数的同名,测试函数运行时自动运行夹具并将夹具的返回值(一个创建好的实例)传递给测试函数,每个测试函数调用时,夹具函数默认都会重新执行一次,以确保每个测试都有一个全新的、不被其他测试影响的实例。
夹具格式:
import pytest #1.导入pytest模块 from survey import AnonymousSurvey #2.导入待测试的类 @pytest.fixture #3.导入装饰器 用于装饰下面的函数 def language_survey(): #4.新建一个用于返回测试类的实例的函数 """一个可供所有测试函数使用的AnonymousSurvey实例"""question = "What language did you first learn to speak?"language_survey = AnonymousSurvey(question) #5.创建一个实例 return language_survey #6.返回该实例 (用于后面测试函数调用)
测试函数:
#接上面def test_store_single_response(language_survey): """测试单个答案会被妥善地存储""" language_survey.store_response('English') assert 'English' in language_survey.responsesdef 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
解析:测试函数形参填入夹具函数language_survey()的同名,pytest会去找名为language_survey的夹具函数,找到后运行测试函数时将该夹具函数的返回值传递给该测试函数,达到一例(实例)多用的目的,注意测试函数的形参名必须与夹具函数的名字一致,不然pytest无法找到夹具。
上篇的记账程序为面向过程的编程,不利于后期扩展,此次改成面向对象编程,需要实现账簿3个功能:①加载数据②记账功能③读取账本功能,最后使用pytest对文件中的类进行测试,需要使用夹具避免测试中重复创建实例
1.将记账程序改为面向对象编程 (原始未修改的完整代码):
from pathlib import Path import jsonprint('==========欢迎使用记账簿===========')class Accountbook: """一个关于记账账本的实现""" def __init__(self, filepath = 'user_information.json'): self.filepath = Path(filepath) self.data = self.load_data() def load_data(self): """加载数据,不存在或损坏时返回空字典""" if self.filepath.exists(): try: user_information = json.loads(self.filepath.read_text()) except json.JSONDecodeError: user_information = {} #内容错误时重置为空字典 else: user_information = {} #文件不存在时新建一个空字典 return user_information def add_records(self): """记账功能""" date = input('请输入日期(如:5/31):').strip() #strip用于删除用户多输入的空格 if date.lower() == 'q': #加lower()防止输入为大写的Q return records = self.data.get(date,[]) #错误写法:records = user_information.get(date,[]) ,这里user_information是load_data里的局部变量,在本方法内还未定义 while True: item = input('商品名称(输入q结束当天):').strip() if item.lower() == 'q': break cost_str = input('花费¥(输入q结束当天):').strip() if cost_str.lower() == 'q': break try: cost = float(cost_str) except ValueError: print('花费请输入数字,本条记录未保存。') #如果不是数字就会不记录并提醒用户输入正确数据 continue records.append({'项目':item,'花费':cost}) if records: #只有在有记录时才保存 self.data[date] = records self.filepath.write_text(json.dumps(self.data,ensure_ascii=False,indent=2)) print('记录已保存。') else: print('当天无记录') def show_records(self): """读取账本功能""" #if先判断文件是否存在,存在则读取,不存在则提醒用户先输入 if self.filepath.exists(): try: contents = self.filepath.read_text() information = json.loads(contents) print(information) return information except json.JSONDecodeError: print('文件损坏,无法读取。') return {} #文件不存在时没有任何返回值,会隐式返回一个None,导致主程序打印None,应返回明确的空字典{} else: #文件不存在时没有任何返回值,会隐式返回一个None,导致主程序打印None,应返回明确的空字典{} print('内容为空,请先记账') return {}account1 = Accountbook()account1.load_data()while True: active = input( '请问是输入还是查看(输入输:i,查看输:o,退出输:q):').strip().lower() if active == 'i': account1.add_records() elif active == 'o': account1.show_records() elif active =='q': break else: print('请按要求输入字母')print('========感谢使用记账簿,欢迎下次使用!==========')
错误解析:上面关于show_records()方法的程序设计有很大缺陷,在默认方法__init__()中已经通过load_data()将文件内容加载到self.data(self.data=load_data()),load_data()已从磁盘文件self.filepath中加载了用户数据,并且记账时(add_records())也维护了self.data,查看账本时在加载一次磁盘文件完全多余,应该直接读取self.data这个内存数据。此处代码修改后如下:
def show_records(self): """读取账本功能(基于内存数据)""" if self.data: print(self.data) return self.data else: print('内容为空,请先记账') return {}
改后代码正常运行
2.测试代码:
import pytestimport jsonfrom pathlib import Pathfrom accountbook import Accountbook # 请确保文件名与你的实际模块名一致@pytest.fixturedef book(): test_file = Path("_test_temp.json") if test_file.exists(): test_file.unlink() obj = Accountbook(filepath=str(test_file)) return objdef test_new_book_has_empty_data(book): """测试1:新创建的账本应无数据且未生成文件""" assert book.data == {} assert not book.filepath.exists()def test_write_and_read_data(book): """测试2:预先给定账本数据,判断是否正确写入文件并可重新加载""" # 给定的测试数据 test_data = { "6/3": [ {"项目": "苹果", "花费": 5.5}, {"项目": "香蕉", "花费": 3.0} ], "6/4": [ {"项目": "可乐", "花费": 2.5} ] } # 直接将数据写入内存属性 book.data = test_data # 手动调用文件写入(模拟保存操作) book.filepath.write_text(json.dumps(book.data, ensure_ascii=False, indent=2)) # 重新创建一个新实例,从同一文件加载 new_book = Accountbook(filepath=str(book.filepath)) # 验证加载的数据与原始数据一致 assert new_book.data == test_data
3.测试结果:
说明:结果报错是因为 accountbook.py 文件中(类所在的文件),在类定义之外(文件末尾)有一段交互式的主程序代码(包含 input 和 while 循环)当 pytest 执行 from accountbook import Accountbook 时,Python 会从头到尾执行该模块的所有顶层代码,结果触发了 input() 等待用户输入,而 pytest 在收集测试时默认会捕获输出,不允许读取标准输入,因此报错。解决方案是将 accountbook.py 末尾的交互式代码放入 if __name__ == '__main__': 条件块中,这样只有在直接运行脚本(python accountbook.py)时才会执行;而被导入(如被 pytest 导入)时则不会执行。
修改前:
修改后:
测试程序运行无异常:
上上篇学习了Python中函数和类的知识
,本篇又学习了要想函数和类正确运行,需要对它们进行测试,写测试代码其实也加深了对函数和类的理解,不要觉得只知道让一个程序能够实现功能就行了,一个好的程序离不开一个好的测试。今天关于《Python编程从入门到实践》这本书算是学完了,下面两周学习numpy及pandas,各位同学和前辈有什么心得或建议欢迎留言一起交流。
谢谢看完,晚安~