Python及Django框架常用的单元测试
一、单元测试
单元测试(unit testing),是指对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。总的来说,单元就是人为规定的最小的被测功能模块。单元测试是在软件开发过程中要进行的最低级别的测试活动,软件的独立单元将在与程序的其他部分相隔离的情况下进行测试。
举个例子:使用Django框架开发项目中,每个数据模型、视图、以及工具函数都可以进行独立的测试,每一个小的测试都可以叫做单元测试。
二、Python中常用单元测试
2.1、断言函数(assert)
断言是Python中单元测试的核心,使用assert
断言是学习Python一个非常好的习惯,assert断言语句格式及用法很简单;在没完善一个程序之前,我们不知道程序在哪里会出错,与其让它在运行时崩溃,不如在出现错误条件时就崩溃,这时候就需要assert断言的帮助。
assert 断言是声明其布尔值必须为真的判定,如果发生异常就说明表达式为假。可以理解 assert断言语句为raise-if-not,用来测试表达式,其返回值为假,就会触发异常。
# 直接使用,遇到错误时直接抛出异常 assert 1==1 assert 2+2 == 2*2 assert len(['my boy', 12]) < 10 assert range(4) == [0,1,2,3] # 加异常信息使用,遇到错误时可以将所加的异常信息显示出来 assert len(lists) >= 5, '列表元素个数小于5' assert 2 == 1, '2不等于1'
Python中的断言用起来非常简单,你可以在assert后面跟上人意判断条件,如果断言失败则会抛出异常;然而用起来并不友好;就比如有人告诉你程序错了,但是不告诉哪里错了;很多时候这样的assert
还不如不写;直接抛一个异常会更好一些。
2.2、断言函数改进
s = 'nothin is impossible.' key = 'nothing' assert key in s, "key: '{}' is not in Target: '{}'".format(key, s) Traceback (most recent call last): File "<stdin>", line 1, in <module> AssertionError: key: 'nothing' is not int Target: 'nothin is impossible.'
看上去还行,但是其实很是有点不友好;假如你是一名测试员,有成千上万的测试案例需要做断言做验证,这样的做法不仅每个断言需要不同的改进,而且一次测试只能测出一个断言错误,执行时只要遇到一个断言错误后就会抛出错误,从而无法将所有的断言错误一次性都检查出来,所以看似简单,但是用于大型项目开发时,就会变得非常麻烦;所以要使用一下对断言方法进行改进的单元测试框架。
2.3、pytest框架
pytest
是一个轻量级的测试框架,所以它压根就没写自己的断言系统,那么也就意味着,用pytest
实现测试,你一行代码都不用改。但是它对Python自带的断言做了强化处理,如果断言失败,那么框架本身会尽可能多地提供断言失败的原因,并且一次能够找出多个断言错误。
import pytest def test_case(): expected = 1 actual = 2 assert expected == actual def test_case11(): expected = 1 actual = 2 assert expected == actual if __name__ == '__main__': pytest.main() # 执行结果: D:\Python3.6\Python36\python.exe C:/Users/Administrator/Desktop/test_s/test_4.py ============================= test session starts ============================= platform win32 -- Python 3.6.5, pytest-6.2.5, py-1.10.0, pluggy-1.0.0 rootdir: C:\Users\Administrator\Desktop\test_s collected 2 items test_4.py FF [100%] ================================== FAILURES =================================== __________________________________ test_case __________________________________ def test_case(): expected = 1 actual = 2 > assert expected == actual E assert 1 == 2 test_4.py:68: AssertionError _________________________________ test_case11 _________________________________ def test_case11(): expected = 1 actual = 2 > assert expected == actual E assert 1 == 2 test_4.py:72: AssertionError =========================== short test summary info =========================== FAILED test_4.py::test_case - assert 1 == 2 FAILED test_4.py::test_case11 - assert 1 == 2 ============================= 2 failed in 12.62s ==============================
使用注意事项:
1.测试文件以test_开头(以_test结尾也可以)
2.测试类以Test开头,并且不能带有 __init__ 方法
3.测试函数以test_开头
4.断言使用基本的assert即可
2.4、unittest框架
Python自带的unittest
单元测试框架就有了自己的断言方法self.assertXXX()
,而且该框架不推荐使用assert XXX
语句,这是Python中比较常用的单元测试框架。
import unittest class TestStringMethods(unittest.TestCase): def test_upper(self): self.assertEqual('foo'.upper(), 'FoO') if __name__ == '__main__': unittest.main() # 执行结果: FoO != FOO Expected :FOO Actual :FoO <Click to see difference> Traceback (most recent call last): File "D:\appinstallation\pycharminstall\PyCharm 2018.3.1\helpers\pycharm\teamcity\diff_tools.py", line 32, in _patched_equals old(self, first, second, msg) File "D:\appinstallation\pythonInstall\lib\unittest\case.py", line 839, in assertEqual assertion_func(first, second, msg=msg) File "D:\appinstallation\pythonInstall\lib\unittest\case.py", line 1220, in assertMultiLineEqual self.fail(self._formatMessage(msg, standardMsg)) File "D:\appinstallation\pythonInstall\lib\unittest\case.py", line 680, in fail raise self.failureException(msg) AssertionError: 'FOO' != 'FoO'
2.5、Django的test框架
Django单元测试框架test.TestCase是继承了python的unittest.TestCase;TestCase也是对unittest.TestCase进行了进一步的封装,省去了很多重复要写的代码,比如定义一个self.client、Email Service提供了方便的邮件发送的方法。
三、Django中单元测试
Django模式是MTV模型,其中T是模板也就是HTML文件,对于HTML来说,没有可测的代码,基本上写死,即使有,并不是重要的逻辑代码。所以在进行单元测试的时候,重点针对M和V展开,也就是models和views。
主要有两种使用模式:
1.使用django框架自带的tests.py文件进行单元测试(常用);
2.自定义创建test.py文件;
这两种是一样,只是运行时所执行目录不一样。
from django.test import TestCase from django_web.models import Event,Guest from django.contrib.auth.models import User # Create your tests here. import datetime get_now = datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S') # 测试数据模型 class DjangoWebModelTest(TestCase): """测试模型""" def setUp(self) -> None: Event.objects.create(id=1,name='小米5',status=True,address='深圳',limit=3,start_time=get_now) Guest.objects.create(id=1,event_id=1,realname='老王',phone=15099925893,email='11@qq.com',sign=False) def test_event_model(self): """测试发布会表""" result = Event.objects.get(name='小米5') self.assertEqual(result.address,'深圳') self.assertTrue(result.status) def test_guest_model(self): """测试嘉宾表""" result = Guest.objects.get(phone='15099925893') self.assertEqual(result.realname,'老王') self.assertFalse(result.sign) # 测试视图 class IndexPageTest(TestCase): """测试index登录首页""" def test_index_page(self): """测试index视图""" response = self.client.get('/index/') self.assertEqual(response.status_code,200) self.assertTemplateUsed(response,'index.html') class LoginAction(TestCase): """测试登录动作""" def setUp(self) -> None: """创建用户数据:两种不同的方式创建用户""" User.objects.create(username='admin') User.objects.create_user(username='admin2',email='admin@11.com',password='123456') def test_add_admin(self): """添加用户admin测试""" user = User.objects.get(username='admin') self.assertEqual(user.username,'admin') def test_add_admin2(self): """添加用户admin2测试""" user = User.objects.get(username='admin2') self.assertEqual(user.username,'admin2') self.assertEqual(user.email,'admin@11.com') def test_login_username_password_null(self): """用户名密码为空""" test_data = {'username':'','password':''} response = self.client.post('/login_action/',data=test_data) self.assertEqual(response.status_code,302) def test_login_username_password_error(self): """用户名密码错误""" test_data = {'username':'test','password':'123456'} response = self.client.post('/login_action/',data=test_data) self.assertEqual(response.status_code,302) def test_login_action_success(self): """登录成功""" test_data = {'username':'admin2','password':'123456'} response = self.client.post('/login_action/',data=test_data) self.assertEqual(response.status_code,302) class EventManageTest(TestCase): """发布会管理""" def setUp(self) -> None: #创建用户账号 User.objects.create_user('admin','admin@qq.com','123456') Event.objects.create(name='小米3',limit=3,address='深圳',status=True,start_time=get_now) self.login_user = {'username':'admin','password':'123456'} #预先登录 self.client.post('/login_action/', data=self.login_user) def test_add_event_data(self): """ 测试添加发布会:小米3 """ event = Event.objects.get(name="小米3") self.assertEqual(event.address, "深圳") def test_event_success(self): """测试发布会:小米3""" response = self.client.post('/event_manager/') self.assertEqual(response.status_code,200) self.assertIn("小米3".encode('utf-8'),response.content) def test_event_search_success(self): """测试发布会搜索""" response = self.client.post('/search_name/') self.assertEqual(response.status_code,200) self.assertIn('小米3'.encode('UTF-8'),response.content) class GuestManageTest(TestCase): """嘉宾管理""" def setUp(self) -> None: User.objects.create_user('admin','admin@qq.com','123456') Event.objects.create(id=1,name='小米2',limit=3,address='深圳',status=True,start_time=get_now) Guest.objects.create(realname='小李子',phone=15099925798,email='11@qq.com',sign=0,event_id=1) self.login_user = {'username':'admin','password':'123456'} #预先登录 self.client.post('/login_action/',data=self.login_user) def test_add_guest(self): """测试添加嘉宾:小李子""" guest =Guest.objects.get(realname='小李子') self.assertEqual(guest.realname,'小李子') self.assertEqual(guest.phone,'15099925798') self.assertEqual(guest.email,'11@qq.com') self.assertFalse(guest.sign) def test_guest_success(self): """测试嘉宾列表:小李子""" response = self.client.post('/guest_manager/') self.assertEqual(response.status_code,200) self.assertIn('小李子'.encode('UTF-8'),response.content) self.assertIn('15099925798'.encode('utf-8'),response.content) def test_guest_search_success(self): """测试嘉宾搜索""" response = self.client.post('/search_phone/') self.assertEqual(response.status_code,200) self.assertIn('小李子'.encode('utf-8'),response.content) self.assertIn('15099925798'.encode('utf-8'),response.content) class SignIndexActionTest(TestCase): """发布会签到""" def setUp(self) -> None: User.objects.create_user('admin','admin@qq.com','123456') Event.objects.create(id=1, name='小米1', limit=3, address='广州', status=True, start_time=get_now) Event.objects.create(id=2, name='小米9', limit=3, address='北京', status=True, start_time=get_now) Guest.objects.create(realname='老张', phone=15099925798, email='11@qq.com', sign=0, event_id=1) #未签到 Guest.objects.create(realname='老周', phone=15099925700, email='22@qq.com', sign=1, event_id=2) #未签到 self.login_user = {'username':'admin','password':'123456'} self.client.post('/login_action/',data=self.login_user) def test_phone_null(self): """测试手机号码为空""" response =self.client.post('/sign_index_action/1/',{"phone":""}) self.assertEqual(response.status_code,200) self.assertIn('请输入电话号码.'.encode('utf-8'),response.content) def test_phone_error(self): """手机号码错误""" response = self.client.post('/sign_index_action/2/',{"phone":"15099925732398"}) self.assertEqual(response.status_code,200) self.assertIn("电话号码错误.".encode('UTF-8'),response.content) def test_phone_or_eventid_error(self): """电话号码所属嘉宾不属于该发布会""" response = self.client.post('/sign_index_action/2/',{"phone":"15099925798"}) self.assertEqual(response.status_code,200) self.assertIn("电话号码所属嘉宾不属于该发布会.".encode('UTF-8'),response.content) def test_already_sign(self): """用户已签到""" response = self.client.post('/sign_index_action/2/',{"phone":"15099925700"}) self.assertEqual(response.status_code,200) self.assertIn("您已经签到!.".encode('utf-8'),response.content) def test_sign_success(self): """签到成功""" response = self.client.post('/sign_index_action/1/',{"phone":"15099925798"}) self.assertEqual(response.status_code,200) self.assertIn("签到成功!".encode('utf-8'),response.content)
# 运行方法: """ 运行所有用例: python3 manage.py test 运行django_web应用下的所有用例: python3 manage.py test django_web 运行sign应用下的tests.py文件用例: python3 manage.py test django_web.tests 运行django_web应用下的tests.py文件中的 DjangoWebModelTest 测试类: python3 manage.py test django_web.tests.DjangoWebModelTest 运行django_web应用下DjangoWebModelTest 测试类中的测试方法(用例): python3 manage.py test django_web.tests.DjangoWebModelTest.test_event_model 模糊匹配测试文件 运行python3 manage.py test django_web -p test*.py ...... # 运行结果 D:\my_django_guest>python3 manage.py test django_web Creating test database for alias 'default'... System check identified no issues (0 silenced). ................... ---------------------------------------------------------------------- Ran 19 tests in 3.080s OK Destroying test database for alias 'default'...
django.test.TestCase类主要由前、后置处理方法 和test开头的方法组成:
test开头的方法 是编写了测试逻辑的用例
setUp方法 (名字固定)在每一个测试方法执行之前被调用
tearDown方法(名字固定) 在每一个测试方法执行之后被调用
setUpClass类方法(名字固定)在整个类运行前执行只执行一次
tearDownClass类方法(名字固定)在调用整个类测试方法后执行一次
from django.test import TestCase class MyTest(TestCase): @classmethod def setUpClass(cls): print('setUpClass') @classmethod def tearDownClass(cls): print('tearDownClass') def setUp(self) -> None: print('setUp') def tearDown(self) -> None: print('tearDown') def test_xxx(self): print('测试用例1') def test_yyy(self): print('测试用例2') # python manage.py test meiduo_mall.apps.users.test_code
注意: 1、最后使用python manage.py test --keepdb进行启动,使用这个命令可以无需重新创建数据库,避免了不加后缀时由于没有数据库权限无法创建数据库的问题;2、settings.py需要配置一下测试数据库名称,这个命令可以自动创建测试数据库和Django系统自带的表,其他表需要在测试数据库中从正式数据库手动复制一份;3、测试过程中数据增删改查都是可以回滚的,就是创建表格和删除表格不能回滚,单元测试里面禁止有创建表格和删除表格的操作;4、此外要注意,测试单元中尽量避免有语法错误,不然会导致在测试数据库中产生的脏数据没法回滚,因为到语法错误处程序就没有办法进行了,直接报错,导致整个框架运行不完整,所以可能会造成脏数据。
出处:https://www.cnblogs.com/liujiajia_me/
本文版权归作者和博客园共有,不得转载,未经作者同意参考时必须保留此段声明,且在文章页面明显位置给出原文连接。