通过前几章的学习,咱们实现了 Todo List 程序的 todo 治理局部,实现了对 todo 的增、删、改、查基本操作,这也是简直所有 Web 程序都具备的性能。咱们当然能够依照目前的思路持续来实现用户治理局部,在 models.py 中编写用户相干的模型,在 templates/ 目录下新建用户相干 HTML,在 controllers.py 中编写用户相干的视图函数。然而,随着新性能的退出,把不同性能的代码都写在雷同的文件中必然会引起代码的凌乱。为实现易保护、易扩大的代码,咱们须要对我的项目的目录构造进行重构。

我的项目重构

目前为止,咱们实现的 Todo List 程序目录构造如下:

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__.pyimport osimport jsonfrom todo.config import BASE_DIRclass Model(object):    """    Model 模型类    """    @classmethod    def _db_path(cls):        """获取存储模型对象数据的文件的绝对路径"""        class_name = cls.__name__        file_name = f'{class_name.lower()}.json'        path = os.path.join(BASE_DIR, 'db', file_name)        return path    @classmethod    def _load_db(cls):        """加载 JSON 文件中所有模型对象数据"""        path = cls._db_path()        with open(path, 'r', encoding='utf-8') as f:            return json.load(f)    @classmethod    def _save_db(cls, data):        """将模型对象数据保留到 JSON 文件"""        path = cls._db_path()        with open(path, 'w', encoding='utf-8') as f:            json.dump(data, f, ensure_ascii=False, indent=4)    @classmethod    def all(cls, sort=False, reverse=False):        """查问全副模型对象"""        # 这一步用来将所有从 JSON 文件中读取的 model 数据转换为 Model 实例化对象,不便后续操作        models = [cls(**model) for model in cls._load_db()]        # 对数据依照 id 排序        if sort:            models = sorted(models, key=lambda x: x.id, reverse=reverse)        return models    @classmethod    def find_by(cls, limit=-1, ensure_one=False, sort=False, reverse=False, **kwargs):        """依据传入条件查问模型对象"""        result = []        models = [model.__dict__ for model in cls.all(sort=sort, reverse=reverse)]        for model in models:            # 依据关键字参数查问 model            for k, v in kwargs.items():                if model.get(k) != v:                    break            else:                result.append(cls(**model))        # 查问给定条数的数据        if 0 < limit < len(result):            result = result[:limit]        # 查问后果集中的第一条数据        if ensure_one:            result = result[0] if len(result) > 0 else None        return result    @classmethod    def get(cls, id):        """通过 id 查问模型对象"""        result = cls.find_by(id=id, ensure_one=True)        return result    def save(self):        """保留模型对象"""        # 查找出除 self 以外所有 model        # model.__dict__ 是保留了所有实例属性的字典        models = [model.__dict__ for model in self.all(sort=True) if model.id != self.id]        # 自增 id        if self.id is None:            # 如果 model_list 大于 0 阐明不是第一条 model,取最初一条 model 的 id 加 1            if len(models) > 0:                self.id = models[-1]['id'] + 1            # 否则阐明是第一条 model,id 为 1            else:                self.id = 1        # 将以后 model 追加到 model_list        models.append(self.__dict__)        # 将所有 model 保留到文件        self._save_db(models)    def delete(self):        """删除模型对象"""        model_list = [model.__dict__ for model in self.all() if model.id != self.id]        self._save_db(model_list)
# todo_list/todo/models/todo.pyfrom . import Modelclass Todo(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 的概率。

日志

Todo List 程序之前记录日志的形式是通过 print 函数来实现的,在前几章的代码中能够找到很多 print 语句。不过 print 函数默认将后果输入到屏幕,而生产环境中通常须要将日志输入到文件中保留下来,不便后续对日志进行剖析。咱们能够通过给 print 函数指定 file 参数(一个文件对象)将其输入内容写入文件。

utils/ 目录下新建 logging.py 用来编写日志记录函数:

# todo_list/utils/logging.pyimport osimport datetimefrom todo.config import BASE_DIRpath = os.path.join(BASE_DIR, 'logs/todo.log')def logger(*args, **kwargs):    """记录日志"""    now = datetime.datetime.strftime(datetime.datetime.now(), '%Y-%m-%d %H:%M:%S')    with open(path, 'a') as f:        # 将日志输入到屏幕,不便调试,上线时可关掉        print(now, '-', *args, **kwargs)        # 将日志输入到文件        print(now, '-', *args, **kwargs, file=f)

接着在 todo_list/ 目录下新建一个 logs/ 目录用来寄存日志。最初将代码中所有 print 语句全副换成 logger 即可。这样日志可能同时输入到屏幕和文件,如果在生产环境则能够只输入到文件。

测试

测试在程序开发过程中占有无足轻重的位置,但很多团队和开发者却对其熟视无睹,以各种理由疏忽测试。尤其是越小的团队越以开发周期短、工夫紧为由省略开发程序的测试过程。但我始终认为这是得失相当的做法,短期内可能放慢了程序开发的进度,但久远来看,前期投入的开发保护精力、老本等将会大大增加。并且生产环境一旦呈现重大破绽,将带来不可挽回的损失。

只管 Todo List 程序十分渺小,但还是要对其退出测试。程序测试方法有很多,如单元测试、功能测试、集成测试等。这里着重介绍下单元测试,单元测试是指对软件中的最小可测试单元进行检查和验证。其中所谓的最小可测单元能够是一个函数、一个类等。

todo_list/ 目录下新建 tests/ 目录用来寄存所有测试文件,测试代码依据被测代码类型的不同别离放到不同文件中,如测试视图函数的代码全副放到名为 test_controllers.py 的文件中,测试模型的代码全副放到名为 test_models.py 的文件中。一个约定俗成的做法是让所有的测试文件名都以 test_ 结尾。

接下来我以测试首页视图函数和新增 todo 视图函数为例,解说测试代码的编写:

# todo_list/tests/test_controllers.pyimport osimport syssys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))from todo.utils.http import Requestfrom todo.controllers import routesfrom todo.models.todo import Tododef test_index():    """测试首页"""    request_message = 'GET / HTTP/1.1\r\nHost: 127.0.0.1:8000\r\n\r\n'    request = Request(request_message)    route, method = routes.get(request.path)    r = route(request)    assert b'Todo List' in bytes(r, encoding='utf-8')    assert b'/new' in bytes(r, encoding='utf-8')def test_new():    """测试新增 todo"""    # 生成随机 todo 内容    content = uuid.uuid4().hex    request_message = f'POST /new HTTP/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()    assert b'302 FOUND' in bytes(r)    assert b'/index' in bytes(r)    assert t.content == contentdef main():    test_index()    test_new()if __name__ == '__main__':    main()

test_index 函数用来测试首页视图函数,为了简化测试代码,测试函数中并没有通过申请 Web Server 来获取响应。首先将申请消息报文 request_message 传递给 Resquest 类结构了一个申请对象,而后依据申请门路 request.path 获取解决该申请的视图函数,接着调用视图函数来获取响应报文。这样做的益处是不须要编写发动申请的客户端程序,但测试覆盖率必定会有所降落。这是一个选择性的问题,须要思考工夫老本、投入产出比等。测试函数的最初通过断言语句,来断言响应报文中必然蕴含的内容。

test_new 函数用来测试新增 todo 视图函数,大体逻辑与 test_index 差不多。在生成测试 todo 内容时应用了 UUID,目标是为了生成足够随机的字符串防止与 db/todo.json 中已存在的 todo 内存反复,这样通过 Todo.find_by() 办法查找 todo 时可能确保查问后果正确。还须要留神的一点是在新增 todo 胜利后又将其删除了,这样做的目标是为了让测试代码不对原有数据产生影响。实践上,测试代码每次执行的后果都应该雷同,并且不应该毁坏程序原有的数据。

应用 Python 运行测试文件 python3 test_controllers.py,如果测试代码执行实现后没有任何输入,就阐明全副测试通过。测试代码遵循 Linux 设计哲学,没有音讯就是最好的音讯。如果测试代码执行过程中抛出 AssertionError 异样,则阐明测试未通过,要么是被测代码有问题,要么是测试代码自身有问题。

因为篇幅所限,对 Todo List 程序的测试局部解说就到这里,其余局部的测试代码能够拜访本章节源码进行查看。

本章源码:chapter7

分割我:

  • 微信:jianghushinian
  • 邮箱:jianghushinian007@outlook.com
  • 博客地址:https://jianghushinian.cn/