• Python项目维护不了?可能是测试没到位。Django的单元测试和集成测试初探


    前言#

    好久没搞 Django 了,最近维护一个我之前用 Django 开发的项目竟然有亲切的感觉😂

    测试,在以前确实是经常被忽略的话题,特别是对于 Python Web 这种快速开发框架,怎么敏捷怎么来,快速开发快速上线,而不是慢工出细活做得很规范,往往也是因为这种粗狂的开发风格,导致项目后续难以维护,这时候再给 Python 冠上一个开发容易维护难的名字。

    Python: 我不背这锅😒

    说回正题,这次的测试包括两部分,在 Django 项目内部写单元测试和集成测试,保证项目功能正常,然后我还开发了一个独立的自动测试工具,可以根据 OpenAPI 的文档来测试,并且在测试完成后输出测试报告,报告内容包括每个接口是否测试通过和响应时间等。

    这个工具我使用了 go 语言开发,主要是考虑到了 go 语言可以傻瓜式的实现交叉编译,生成的可执行文件直接上传到服务器就可以执行,非常方便。

    这个自动测试工具我会在下一篇文件介绍。

    Django 的测试#

    不得不说 Django 的文档写得真不错👍

    我看了一会文档就开始写测试,Django 全家桶真的舒服,开发体验太丝滑了

    使用 startapp 创建 app 的时候,每个 app 目录下都有个 tests.py 文件,我们的测试代码就写在这个文件里面好了。

    如果测试代码很多的话,还可以拆分,如何拆分参考 views.py 的拆分,把 tests.py 改成 package ,即创建个 tests 目录,下面放各个测试文件,然后在 __init__.py 里引入。

    测试分为单元测试和集成测试,在 Django 里写单元测试比 AspNetCore 舒服多了,不用考虑依赖注入的问题,Django 全给你处理好了。

    在测试的时候,Django 会自动创建测试数据库,因此也不用自己去折腾环境隔离啥的。

    关于这俩种测试的区别,我之前的文章里有介绍,就不复制粘贴了。

    Asp-Net-Core学习笔记:单元测试和集成测试

    例子#

    这次以两个 app 为例

    • config - 单元测试
    • dashboard - 集成测试

    在 Django 里这俩种测试都没啥心智负担,也不用啥额外的操作,直接在 tests.py 里写就完事了。

    单元测试#

    以 config app 作为单元测试的例子

    我封装了一个 ConfigService 代码如下

    from datetime import date
    from .models import CommonConfig
    
    
    class ConfigService(object):
        def __init__(self):
            ...
    
        @staticmethod
        def get_config(key: str) -> str:
            queryset = CommonConfig.objects.filter(key=key)
            if queryset.exists():
                return queryset.first().value
            return ''
    
        @property
        def today(self):
            return date.today()
    
        @property
        def start_year(self):
            value = self.get_config('start_year')
            return str(self.today.year) if len(value) == 0 else value
    
        @property
        def start_month(self):
            value = self.get_config('start_month')
            return str(self.today.month) if len(value) == 0 else value
    
        @property
        def end_year(self):
            value = self.get_config('end_year')
            return str(self.today.year) if len(value) == 0 else value
    
        @property
        def end_month(self):
            value = self.get_config('end_month')
            return str(self.today.month) if len(value) == 0 else value
    
    

    编辑 apps/config/tests.py

    from django.test import TestCase
    from apps.config.models import CommonConfig
    from apps.config.services import ConfigService
    
    
    class CommonConfigTestCase(TestCase):
        def setUp(self):
          CommonConfig.objects.create(key='start_year', value='2023')
          CommonConfig.objects.create(key='end_year', value='2024')
          CommonConfig.objects.create(key='start_month', value='1')
          CommonConfig.objects.create(key='end_month', value='10')
    
        def test_common_config(self):
          cfg = ConfigService()
          self.assertEqual(cfg.start_year, '2023')
          self.assertEqual(cfg.end_year, '2024')
          self.assertEqual(cfg.start_month, '1')
          self.assertEqual(cfg.end_month, '10')
    

    集成测试#

    集成测试是模拟 HTTP 请求去访问接口,看看接口正不正常。

    Django 的 TestCase 类里自带了 client 属性,可以很方便的请求接口。

    一般接口都要登录才能用,然后 client 里也很贴心的集成了 Django 的认证授权体系,直接 login 就完事了。

    注意测试的时候是自动创建了临时数据库,所以得先添加用户。

    接着调用 self.client.get, self.client.post 之类的方法去测试接口就好了。

    from django.test import TestCase
    from django.shortcuts import reverse
    from django.contrib.auth.models import User
    from rest_framework import status
    
    
    class DashboardTests(TestCase):
        def setUp(self):
            User.objects.create_user(username='user', password='pwd')
            self.client.login(username="user", password="pwd")
    
        def test_overview(self):
            resp = self.client.get(reverse('dashboard:overview'), {'grant_year': 2023, 'grant_month': 2})
            self.assertEqual(resp.status_code, status.HTTP_200_OK)
            self.assertEqual(resp.json()['code'], status.HTTP_200_OK)
    
        def test_monthly_data(self):
            resp = self.client.get(reverse('dashboard:monthly_data'), {'grant_year': 2023, 'grant_month': 2})
            self.assertEqual(resp.status_code, status.HTTP_200_OK)
            self.assertEqual(resp.json()['code'], status.HTTP_200_OK)
    
        def test_county_data(self):
            resp = self.client.get(reverse('dashboard:county_data'), {'grant_year': 2023, 'grant_month': 2})
            self.assertEqual(resp.status_code, status.HTTP_200_OK)
            self.assertEqual(resp.json()['code'], status.HTTP_200_OK)
    
    

    运行测试#

    测试写完之后

    使用命令运行测试

    python manage.py test
    

    还有其他参数请参考官方文档

    不过运行测试的时候就没有 AspNetCore 爽了,没有那种一个个接口相继亮起绿灯的快感;

    Django 的测试只能看到测试结果,有多少个测试通过了,如果有报错会看到 Traceback 信息。

    $ python .\manage.py test 
    Found 5 test(s).
    Creating test database for alias 'default'...
    System check identified no issues (0 silenced).
    .....
    ----------------------------------------------------------------------
    Ran 5 tests in 0.831s
    
    OK
    Destroying test database for alias 'default'...
    

    小结#

    Django 还是熟悉的味道,好用就对了。

    参考资料#

    没想到不知不觉中 Django 刷版本号到 5.0 了…

  • 相关阅读:
    Centos7实操练习题
    x86汇编_压缩BCD码_DAA指令_DAS指令_笔记57
    Linux 编译链接那些事儿(02)C++链接库std::__cxx11::basic_string和std::__1::basic_string链接问题总结
    Docker 的数据管理 端口映射 容器互联 镜像的创建
    从裸机到嵌入式Linux—总纲
    将json数据转换为Python字典
    window mysql忘记密码解决方案
    【通信工程笔记】【终端与业务-第十二章】市场营销计划、实施和控制
    C++:位图
    VMLogin防关联指纹浏览器与传统浏览器的主要区别
  • 原文地址:https://www.cnblogs.com/deali/p/18057931