通过前面几章的学习,我们完成了TodoList程序的todo管理部分,实现了增删改查待办事项。修改和检查基本操作,这也是几乎所有网页程序都有的功能。当然,我们可以继续按照目前的思路实现用户管理部分,在models.py中编写用户相关的模型,在templates/目录中新建用户相关的HTML,在controller中编写用户相关的视图函数。py。但是,随着新功能的加入,将不同功能的代码写在同一个文件中,难免会造成代码混乱。为了实现代码易于维护和扩展,我们需要重构项目的目录结构。项目重构至此,我们实现的TodoList程序目录结构如下:todo_list├──server.py└──todo├──__init__.py├──config.py├──controllers.py├──db│└──todo.json├──models.py├──static│├──css││└──style.css│└──favicon.ico├──templates│├──edit.html│└──index.html└──utils.py重构后的目录结构如下:todo_list├──server.py└──todo├──__init__.py├──config.py├──controllers│├──__init__.py│├──static.py│└──todo.py├──db│└──todo.json├──models│├──__init__.py│└──todo.py├──static│├──css││└──style.css│└──favicon.ico├──templates│└──todo│├──edit.html│└──index.html└──utils├──__init__.py├──http.py└──templating.py首先将原来的controllers.py文件替换成controllers/包,将视图函数按照其在controllers/目录下的功能分到不同的文件中,放置在这些视图函数中被组合在一起呃在controllers/__init__.py中。将读取静态资源的视图函数static和读取网页ICO图标的视图函数favicon放入controllers/static.py,将todo相关的视图函数放入controllers/todo.py。同样,将models.py文件替换为models/包,将原来的Todo模型类放到models/todo.py中。但是这里不仅仅是简单的迁移了原来的Todo模型代码,而是对其进行了重构,抽象出一个模型基类Model放在models/__init__.py中,然后Todo继承自Model模型基类。这样做的好处是,我们在写用户模型的时候,不需要再在用户模型中写查找、保存等方法。我们只需要让用户模型也继承Model模型基类即可。#todo_list/todo/models/__init__.pyimportosimportjsonfromtodo.configimportBASE_DIRclassModel(object):"""Model模型类"""@classmethoddef_db_path(cls):"""获取存放模型对象数据的文件绝对路径"""class_name=cls.__name__file_name=f'{class_name.lower()}.json'path=os.path.join(BASE_DIR,'db',file_name)返回路径@classmethoddef_load_db(cls):"""加载JSON文件中的所有模型对象数据"""path=cls._db_path()withopen(path,'r',encoding='utf-8')asf:returnjson.load(f)@classmethoddef_save_db(cls,data):"""将模型对象数据保存到JSON文件"""path=cls._db_path()withopen(path,'w',encoding='utf-8')asf:json.dump(data,f,ensure_ascii=False,indent=4)@classmethoddefall(cls??,sort=False,reverse=False):"""查询所有模型对象"""#这一步用来转换所有对象从JSON文件中读取的模型数据,转换为Model实例化对象,方便后续操作models=[cls(**model)用于cls._load_db()中的模型]#按id排序数据ifsort:models=sorted(models,key=lambdax:x.id,reverse=reverse)returnmodels@classmethoddeffind_by(cls,limit=-1,ensure_one=False,sort=False,reverse=False,**kwargs):"""根据传入条件查询模型对象"""result=[]models=[model.__dict__formodelincls.all(sort=sort,reverse=reverse)]formodelinmodels:#根据关键字参数查询模型fork,vinkwargs.items():ifmodel.get(k)!=v:breakelse:result.append(cls(**model))#queryforAcertainnumberofdataif00elseNonereturnresult@classmethoddefget(cls,id):"""通过id查询模型对象"""result=cls.find_by(id=id,ensure_one=True)returnresultdefsave(self):"""保存模型对象"""#查找除self之外的所有模型#model.__dict__是保存所有实例属性的字典models=[model.__dict__formodelinself.all(sort=True)ifmodel.id!=self.id]#自增idifself.idisNone:#如果model_list大于0说明不是第一个模型,取最后一个模型的id加1iflen(models)>0:self.id=models[-1]['id']+1#否则为第一个模型,id为1else:self.id=1#添加当前模型到model_listmodels.append(self.__dict__)#将所有模型保存到文件self._save_db(models)defdelete(self):"""删除模型对象"""model_list=[model.__dict__formodelinself.all()ifmodel.id!=self.id]self._save_db(model_list)#todo_list/todo/models/todo.pyfrom.importModelclassTodo(Model):"""Todo模型类"""def__init__(self,**kwargs):self.id=kwargs.get('id')self.content=kwargs.get('content','')添加到存放HTML模板的templates/目录todo/目录用于存放与todo相关的HTML模板。原来的工具集utils.py文件也换成了一个Python包,根据不同类型的工具代码放到不同的文件中。请求类Request、响应类Response和重定向函数redirect都放在utils/http.py中。模板引擎类Template和渲染模板函数render_template放在utils/templating.py中。至此,项目目录结构重建完成。大多是将原来的单个文件改成Python包的形式,这样可以更好的组织代码结构。在写用户管理功能之前,先介绍一下web开发过程中的两个重要环节,日志和测试。日志和测试是保证生产环境项目稳定运行的重要保障。日志可以记录程序的异常信息,监控和分析程序的运行状态。测试可以有效降低程序在生产环境出现bug的概率。日志记录之前在TodoList程序中的日志记录方式是通过打印函数,在前面章节的代码中可以找到很多打印语句。但是print函数默认是将结果输出到屏幕上,而在生产环境中,通常需要将日志输出到文件中保存,以供后续分析日志使用。我们可以通过为打印函数指定文件参数(文件对象)来将打印函数的输出写入文件。在utils/目录下新建一个logging.py来编写日志函数:(*args,**kwargs):"""Logging"""now=datetime.datetime.strftime(datetime.datetime.now(),'%Y-%m-%d%H:%M:%S')withopen(path,'a')asf:#将日志输出到屏幕上方便调试,上线时可以关闭print(now,'-',*args,**kwargs)#输出日志log到文件print(now,'-',*args,**kwargs,file=f)然后在todo_list/目录下创建一个logs/目录用来存放日志。最后,将代码中的所有打印语句替换为logger。这样就可以将日志同时输出到屏幕和文件中。如果是在生产环境,只能输出到文件中。测试测试在程序开发过程中扮演着重要的角色,但是很多团队和开发人员却因为种种原因对其视而不见,忽视了测试。尤其是越小的团队,越以开发周期短、时间紧为由,省略了开发程序的测试过程。但我始终认为这得不偿失。短期内可能会加快程序开发的进度,但从长远来看,后期投入的开发维护精力和成本会大大增加。而一旦生产环境出现严重漏洞,将带来无法挽回的损失。尽管TodoList程序很小,但值得对其添加测试。程序测试的方法有很多种,如单元测试、功能测试、集成测试等,这里重点介绍单元测试,它是指检查和验证软件中最小的可测试单元。所谓最小的可度量单元可以是一个函数,一个类等等。在todo_list/目录下新建tests/目录,存放所有测试文件。测试代码根据测试代码的类型放在不同的文件中。例如,测试视图函数的所有代码都放在一个名为test_controllers.py的文件中。测试模型的代码都放在一个名为test_models.py的文件中。通常的做法是让所有测试文件名都以test_开头。下面以测试首页视图函数和新增todo视图函数为例,讲解测试代码的编写:#todo_list/tests/test_controllers.pyimportosimportsyssys.path.insert(0,os.path.dirname(os.path.dirname(os.path.abspath(__file__))))fromtodo.utils.httpimportRequestfromtodo.controllersimportroutesfromtodo.models.todoimportTododeftest_index():"""测试主页"""request_message='GET/HTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\n'request=Request(request_message)路由,method=routes.get(request.path)r=route(request)assertb'TodoList'inbytes(r,encoding='utf-8')assertb'/new'inbytes(r,encoding='utf-8')deftest_new():"""测试新待办事项"""#生成随机todo内容content=uuid.uuid4().hexrequest_message=f'POST/newHTTP/1.1\r\nHost:127.0.0.1:8000\r\n\r\ncontent={content}'request=Request(request_message)route,method=routes.get(request.path)r=route(request)t=Todo.find_by(content=content,ensure_one=True)t.delete()以字节为单位断言b'302FOUND's(r)assertb'/index'inbytes(r)assertt.content==contentdefmain():test_index()test_new()if__name__=='__main__':main()test_index函数用于测试首页view函数,为了简化测试代码,测试函数不通过请求WebServer获取响应。首先将请求消息messagerequest_message传递给Resquest类构造请求对象,然后根据请求路径获取处理请求的视图函数request.path,然后调用视图函数获取响应消息.这样做的好处是不用写发起请求的客户端程序,但是测试覆盖率肯定会下降。这是一个选择性的问题,需要考虑时间成本、投入产出比等。在测试函数的最后,断言语句用于断言响应消息中必须包含的内容。test_new函数用于测试新增的todo视图函数,大体逻辑与test_index类似。UUID在生成测试todo内容时使用,目的是生成一个足够随机的字符串,避免与db/todo.json中已有的todo内存重复,从而在查找todo时保证查询结果正确通过Todo.find_by()方法。还需要注意的是,todo添加成功后,是被删除的。这样做的目的是防止测试代码影响原始数据。理论上,每次执行测试代码的结果应该是一样的,不应该破坏程序原有的数据。使用Python运行测试文件python3test_controllers.py。如果测试代码执行后没有输出,说明所有测试都通过了。测试代码遵循Linux设计理念,没有消息就是最好的消息。如果在测试代码执行过程中抛出AssertionError异常,则说明测试失败,要么是被测代码有问题,要么是测试代码本身有问题。限于篇幅,TodoList程序测试部分的讲解到此结束。其他部分测试代码可以访问本章源码查看。本章源码:chapter7联系我:微信:jianghushinian邮箱:jianghushinian007@outlook.com博客地址:https://jianghushinian.cn/