Django单元测试
本文将会讨论什么是单元测试、单元测试的意义、django中是如何进行单元测试的
认识单元测试
单元测试就是用一段代码去测试另一段代码。测试的目标是软件设计的最小单位,比如函数或者一个类的方法。它的测试用例是基于白盒测试来设计的(知道程序设计的内部逻辑结构),也就是说单元测试的目的在于发现各模块内部可能存在的错误。
为什么要编写单元测试?
你可以跟别人说你在编程这方面很有经验,但是你不能向别人保证你的代码一定没有错,因为你没有证据。在别人发现你的错误之前,编写测试会让你得到一次提前修改的机会。其中的道理请详看Django单元测试
单元测试的意义
1.提高开发效率
2.保证代码质量
3.特殊意义
提高开发效率
以前后端分离开发为例,服务端与客户端是并行开发的。假设服务端已经开发好了,但是客户端的界面还没做完。这时候怎么确定我开发的接口是不是正确的呢?如果等客户端写完了,再来调用我的接口,或者是自己手动调用开发好的模块,再Print返回值跟预期值人肉的做对比。这两种方式都太影响开发效率了。
在单元测试中,会根据模块的逻辑结构编写通用的测试用例。再用assert断言来判断被测试方法返回结果是否符合你的预期值。当模块通过了单元测试,也就意味着模块是正确的(单元测试没有写错的情况下)。这时候负责客户端的哥们就可以直接拿模块对应的接口直接调用了。
保证代码的质量
在项目开发中因为版本更新或者客户的新需求,不断的修改代码是在所难免的。这时候就很容易“牵一发而动全身”。为了实现新需求,修改了之前写好的方法,可能导致某个模块甚至整个项目都崩了。显然,代码质量是不合格的。
如果各模块有与之对应的单元测试。每一次修改后,只需要跑一遍单元测试,有没有新的BUG一目了然。
特殊意义
对于刚入门的新手,从编写单元测试开始可以快速上手开发。因为单元测试的本质是用一段代码去测试另一段代码的逻辑。在编写测试代码前,是需要先了解被测模块的执行流程和详细逻辑。
让一个新手自己从零开始开发一个模块需要很多时间,但是让他去读一遍开发好的模块代码并不需要很多时间,这也是一种学习的方式。就好像创作一篇文章很难,但是多看别人的文章之后,会发现自己也能写了。
Django中的单元测试
Python中最常用的单元测试方法是unittest.TestCase,而Django中的django.test.Testcase是在前者的基础上进一步的封装。
Django的设计模式是MTV,所以主要测试的是模型跟视图,也就是数据库模型跟代码逻辑。
测试数据库模型
下面以一个简单的用户注册表为例
模型
class AccountRegister(models.Model):
"""
用户注册表
"""
user_name = models.CharField(max_length=50, blank=True, null=True)
password = models.CharField(max_length=50, blank=True, null=True)
sex = models.IntegerField(max_length=10, blank=True, null=True)
register_time = models.DateTimeField(blank=True, null=True) # 注册时间
对应的单元测试
class AccountTestCase(TestCase):
def setUp(self):
"""
初始化数据
"""
self.account = models.AccountRegister.objects.create(
user_name='test',
password='root',
sex=1, # 1-男性 2-女性
register_time='2021-03-14 18:20:00'
)
def test_account_model(self):
"""
测试方法之一
"""
self.assertEqual(self.account.register_time, '2021-03-14 18:20:00')
执行命令:
python manage.py test # 执行整个项目的单元测试
python manage.py test account # 只执行account模块
python manage.py test account.tests.XxxTestCase # 只执行某一个测试类
似乎看起来意义不大,但是实际项目中会有几十个模型,大型项目中甚至会有上百个模型。
假设某天项目负责人换了,觉得数据库中的性别字段存的都是111222不够直观,就把字段类型改成varchar,直接存“男、女”。然后把原来的数据替换更新。但是之前涉及到性别判断的模块用的都是整型,那项目运行的时候就报错了。
这个例子可能不够真实,试想实际项目是有很多个人一起开发的,张三改一下,李四改一下,每个人都只顾着自己负责的模块能不能正常运行。张三改了一个地方,李四的功能可能就崩了,整个项目也受影响。
测试接口
from rest_framework.test import APITransactionTestCase, RequestsClient
class XXXTestCase(APITransactionTestCase):
def setUp(self) -> None:
"""
初始化数据
"""
# 如果接口需要认证,则设置Token
self.token = 'your_token'
self.client = RequestsClient()
self.client.headers.update({'X-TOKEN': self.token})
self.BASE_URL = 'http://127.0.0.1:8000'
def test_delete_xxx(self):
id = "test_id"
api = '/test/{}/'.format(id) # 你的接口
response = self.client.delete(self.BASE_URL + api)
resp = response.json()
self.assertEqual(resp['code'], 200) # 根据情况断言返回结果
self.assertEqual(resp['message'], 'ok')
这样的好处是每次测试只需要跑一遍单元测试即可,如果手动打开网页或者Postman之类的工具,每次测试之前你都需要把你的Django后台运行起来,然后再输入api以及参数。试想,如果这是一个注册接口,对应的表单有很多字段,每次修改完代码,测试的时候都需要手动输入这些参数,就特别费劲了。
更多测试实践请点击链接跳转详看Django单元测试最佳实践
Django单元测试中的地雷
Django每次运行单元测试时,都会临时新建一个数据库,首先是执行Setup方法,有多少个测试方法就执行多少次Setup方法进行初始化数据。执行完之后,就会把刚才的数据全部清空。这看着是一个优点,测试不影响实际数据库中的数据,但是如果Settiing文件中设置的测试数据库名与生产环境的数据库名一样(如下所示)并且执行的时候没有 - -keepdb 参数
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 'test', # 数据库名
'USER': 'root', # 用户
'PASSWORD': 'root', # 密码
'HOST': '127.0.0.1', # 地址
'PORT': '3306', # 端口
'TEST': { # 测试数据库配置
'NAME': 'test',
}
},
}
单元测试执行完后,就会把生产环境中的数据库删除!!!
现实案例
报错:
原因: