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、此外要注意,测试单元中尽量避免有语法错误,不然会导致在测试数据库中产生的脏数据没法回滚,因为到语法错误处程序就没有办法进行了,直接报错,导致整个框架运行不完整,所以可能会造成脏数据。

作者:E-QUAL
出处:https://www.cnblogs.com/liujiajia_me/
本文版权归作者和博客园共有,不得转载,未经作者同意参考时必须保留此段声明,且在文章页面明显位置给出原文连接。
                                            本文内容参考如下网络文献得来,用于个人学习,如有侵权,请您告知删除修改。
                                            参考链接:https://www.cnblogs.com/linhaifeng/
                                                             https://www.cnblogs.com/yuanchenqi/
                                                             https://www.cnblogs.com/Eva-J/
                                                             https://www.cnblogs.com/jin-xin/
                                                             https://www.cnblogs.com/liwenzhou/
                                                             https://www.cnblogs.com/wupeiqi/
posted @ 2021-10-03 18:12  E-QUAL  阅读(725)  评论(0编辑  收藏  举报