Django 单元测试遇到的问题

单元测试框架unittest

unittest是python自带的一个单元测试框架,无需安装,使用简单方便。

unittest中最核心的部分是:TestFixture、TestCase、TestSuite、TestRunner

unittest case的运行流程:

  1. 写好一个完整的TestCase
  2. 多个TestCase 由TestLoder被加载到TestSuite里面, TestSuite也可以嵌套TestSuite
  3. 由TextTestRunner来执行TestSuite,测试的结果保存在TextTestResult中
  4. TestFixture指的是环境准备和恢复

TestFixture

用于测试环境的准备和恢复还原, 一般用到下面几个函数:

  1. setUp():每个测试case运行之前运行
  2. tearDown():每个测试case运行完之后执行
  3. setUpClass():必须使用@classmethod 装饰器, 所有case运行之前只运行一次
  4. tearDownClass():必须使用@classmethod装饰器, 所有case运行完之后只运行一次

TestCase

  • 执行结果:成功是 .,失败是 F,出错是 E,跳过是 S
  • 测试用例的执行跟方法的顺序没有关系, 默认按字母顺序
  • 每个测试方法均以 test 开头

测试model文件:

# -*- coding: utf-8 -*-
import unittest


class TestMy(unittest.TestCase):
    def setUp(self):
        # 每个测试用例运行之前执行
        print 'setUp'

    def tearDown(self):
        # 每个测试用例运行之后执行
        print 'tearDown'

    @classmethod
    def setUpClass(cls):
        # 所有测试用例运行之前执行
        print 'setUpClass'

    @classmethod
    def tearDownClass(cls):
        # 所有测试用例运行之后执行
        print 'tearDownClass'

    def run_test(self):
        # 不是以test开头,不是测试用例
        print 'run'

    def test_run_a(self):
        # 测试用例
        self.assertEqual(1, 1)

    def test_run_b(self):
        # 测试用例
        self.assertEqual(1, 2)

在单元测试运行完之后,成功会打印一个".",失败会显示断言失败的地方。

Django单元测试

django的单元测试模块是基于unittest编写。我们使用的类名为django.test.TestCase,继承于python的unittest.TestCase。所以它的整个流程跟unittest是完全一致的。

  • 不同点:在Django.test模块中定义了Client类用于在测试工程中模拟多种请求的发送。
from django.test import TestCase
import json

class TestAlarm(TestCase):

      def test_nic_type(self):
        # 测试获取NIC类型接口
        r = self.client.get('/api/home/nic_type/')
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content)
        self.assertEqual(content['result'], True)


Django支持的请求类型

请求类型 调用方法
Post请求 post(path, data=None, content_type=MULTIPART_CONTENT, follow=False, secure=False, **extra)
Get请求 get(path, data=None, follow=False, secure=False, **extra)
Put请求 put(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
Patch请求 patch(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)
Delete请求 delete(path, data='', content_type='application/octet-stream', follow=False, secure=False, **extra)

常用参数含义:

参数名 含义解释
path 发送请求使用url
data 发送请求时携带的数据
content_type 携带数据的格式
secure 客户端将模拟HTTPS请求
follow 客户端将遵循任何重定向

其中:post、put、patch、delete请求方法中content_type为application/json时,如果data为dict,list或tuple类型,则需要使用json.dumps()进行序列化。

请求数据的接收与处理

通过Client对象发出的请求在经过被测试单元的一系列操作之后,返回的数据会以返回值的形式返回。一般情况下,后端返回的数据形式为json格式,在接收数据后需要调用json方法进行解析。

import json

 def test_nic_type(self):
       # 测试获取NIC类型接口
       response = self.client.get('/api/home/nic_type/')
       self.assertEqual(response.status_code, 200)
       content = json.loads(response.content)

其中,response的常用属性有:

  • status_code:http状态码
  • content: reponse的body,json格式,用json方式进行解析

结果验证

  • self.assertEqual(1, 2): 断言1==2
  • self.assertTrue(isinstance(content, list)): 断言content的类型是否是list
  • self.assertContains(response, text, count=None, status_code=200, msg_prefix='', html=False): 断言response是否与status_code和text内容相应。将html设为True会将text作为html处理。
  • self.assertJSONEqual(raw, expected_data, msg=None):断言Json片段raw和expected_data是否相当。

mock数据

mock 是辅助单元测试的模块,用于测试不方便调用的别人的接口。

使用:

在python2.x中,mock是一个独立的模块,使用时需要先安装

pip install mock

在python3.3之后,mock已经合并到unittest模块中了,是unittest单元测试的一部分,直接导入过来就行

from unittest import mock

使用举例

需要测试的方法:

    def get_os_account(cls, bk_biz_id):
        params = {
            'bk_biz_id': bk_biz_id
        }
        accounts = JobApi.get_os_account(params=params)
        return accounts

其中JobApi.get_os_account为外部接口,单元测试时无法调用,所以需要使用mock数据,使用方法如下:

import mock

def test_get_os_account(self):
    """
    测试方法get_os_account
    :return:
    """
    JobApi.get_os_account = mock.MagicMock(return_value=Mock_Os)
    data = Rules.get_os_account(
        bk_biz_id=2
    )
    self.assertTrue(isinstance(data, dict))

其中Mock_Os为:

Mock_Os = {
    "result": True,
    "code": 0,
    "message": "success",
    "data": [
        {
            "id": 2,
            "account": "Administrator",
            "creator": "system",
            "os": "Windows",
            "alias": "Administrator",
            "bk_biz_id": 2,
            "create_time": "2018-03-22 15:36:31"
        }
    ]
}

数据库

测试是需要数据库的,django会为测试单独生成数据库。不管你的测试是否通过,当你所有的测试都执行过后,这个测试数据库就会被销毁。

  • 注意要点: 在执行单元测试的过程中,会Django会生成临时的数据库, 因此,连接数据库的用户需要有创建和删除数据库的权限。
  • 默认情况下,测试数据库的名字是test_DATABASE_NAME,DATABASE_NAME是你在settings.py里配置的数据库名;
  • 如果你需要给测试数据库一个其他的名字,可以在settings.py中指定TEST_DATABASE_NAME的值

配置测试数据库,在setting.py文件DATABASES中增加TEST字段

DATABASES = {
    'default': {
        'ENGINE': 'django.db.backends.mysql',  # 默认用mysql
        'NAME': 'hn_collector',                        # 数据库名 (默认与APP_ID相同)
        'USER': 'root',                        # 你的数据库user
        'PASSWORD': '123456',                    # 你的数据库password
        'HOST': '127.0.0.1',                   # 开发的时候,使用localhost
        'PORT': '3306',                        # 默认3306
        'TEST': {
            'NAME': 'data_collector_test',
            'CHARSET': 'utf8',
            'COLLATION': 'utf8_general_ci'
        },
        'OPTIONS': {'charset': 'utf8'},
    },
}

推荐使用配置数据库,这个可以修改测试数据库的相关配置。否则会比较容易出问题:例如编码问题:中文无法入库等

keepdb

在编写单元测试时,每次重新创建MySQL数据库表都花费很长时间,于是在表结果不变的情况下,想使用keepdb参数

python manage.py test --keepdb

测试数据:fixtures

有一些项目是基于数据库来开发的,如果数据库里没有数据,那么对于一个基于数据库的网站来说,test case并无多大的用处.为了给测试数据库加入测试数据更方便,django提供了载入fixtures的方法.

fixture是一系列的数据集合,django知道如何将它导入数据库。

使用步骤
  • 在myapp下创建fixtures文件夹,在文件夹下创建myapp.json文件
  • 在json文件中配置
[{
    "fields": {
        "subnets": "1",
        "update_time": "2020-03-13T16:48:15",
        "name": "test",
        "ch_operator": null,
        "scan_time": "2020-03-13T18:31:30",
        "edit_user": null,
        "is_delete": 0,
        "create_user": "wangying",
        "create_time": "2020-03-13T16:48:15"
    },
    "model": "tasks.tasksconfig",
    "pk": 3
}, {
    "fields": {
        "status": 1,
        "edit_time": "2020-03-13T16:48:15",
        "is_delete": 0,
        "create_user": "wangying",
        "create_time": "2020-03-13T16:48:15",
        "frequency": 60,
        "time": "02:00:00",
        "day": 5,
        "tasksconfig": 3
    },
    "model": "tasks.cycle",
    "pk": 3
}]

  • 如果实际数据库中有数据,可以将数据直接拷贝到myapp.json文件中,拷贝命令如下
python manage.py dumpdata myapp >myapp/fixtures/myapp.json
  • setting.py中加入:FIXTURE_DIRS = ('/path/to/api/fixtures/',)
  • 在测试文件中使用:
class TestHome(BaseTestCase):
    fixtures = ['ip_address.json']

    def setUp(self):
        super(TestHome, self).setUp()

登陆验证

直接将登陆验证mock掉,可以绕过中间件鉴权(不推荐)

@patch('account.middlewares.LoginMiddleware',return_value={'user': 'wy'})
    def test_all_sub(self, user):

直接cookie赋值

    def setUp(self):
        self.client.cookies['bk_token'] = 'bk_token'

Celery 单元测试

异步任务单元测试

简单例子:

@task()
def add():
    print '33333'
    def task_celery(self, request):
        type = request.GET.get('type')
        if int(type) == 1:
            add.delay()
            data = {
                'status': 1
            }
        else:
            data = {
               'status': 2
            }
        return Response(data)

单元测试方法:使用CELERY_ALWAYS_EAGER=True

  • 装饰器override_settings
from django.test.utils import override_settings

    @override_settings(CELERY_ALWAYS_EAGER=True)
    def test_add(self):
        param = {
            'type': 1
        }
        r = self.client.get('/api/task/task_celery/', param, content_type='application/json')
        self.assertEqual(r.status_code, 200)

        content = json.loads(r.content)
        self.assertEqual(content['result'], True)
  • 直接修改settings
from django.conf import settings

    def setUp(self):
        settings.CELERY_ALWAYS_EAGER = True

如果设置CELERY_ALWAYS_EAGER=True,则下面两行代码没有区别

add.delay()
add()

文件上传:

只需提供文件字段名称作为键,并提供要上传的文件的文件句柄作为值。

    def test_upload_file(self):
        """
        测试方法 upload_file
        :return:
        """
        with open('apps/test_handlers/mock_data/upload_file/ydauto_tomcat.zip') as fb:
            r = self.client.post('/api/scripts/', data={'file': fb})
            content = json.loads(r.content)
            self.assertEqual(r.status_code, 200)

file只是一个键值,可以自己定义

运行单元测试

  • 测试整个工程python manage.py test
  • 测试某个应用python manage.py test app_name
  • 只测试一个Casepython manage.py test MyTestCase
  • 只测试一个方法python manage.py test test_func

测试通过:

猜你喜欢

转载自www.cnblogs.com/wangyingblock/p/12478512.html