Django 单元测试
创始人
2025-05-29 07:25:46
0

1. 环境准备

1、创建项目和应用:

django-admin startproject django_example_untest
cd django_example
python manage.py startapp users

2、添加应用,注释 csrf

INSTALLED_APPS = ['django.contrib.admin','django.contrib.auth','django.contrib.contenttypes','django.contrib.sessions','django.contrib.messages','django.contrib.staticfiles','users'
]
MIDDLEWARE = ['django.middleware.security.SecurityMiddleware','django.contrib.sessions.middleware.SessionMiddleware','django.middleware.common.CommonMiddleware',# 'django.middleware.csrf.CsrfViewMiddleware',		# 防止测试时需要验证 csrf'django.contrib.auth.middleware.AuthenticationMiddleware','django.contrib.messages.middleware.MessageMiddleware','django.middleware.clickjacking.XFrameOptionsMiddleware',
]

3、目录结构如下:

django_example
├── django_example_untest
│   ├── __init__.py
│   ├── settings.py
│   ├── urls.py
│   └── wsgi.py
├── manage.py
└── users├── __init__.py├── admin.py├── apps.py├── migrations│   └── __init__.py├── models.py├── tests.py└── views.py

4、添加视图函数 users/views.py

import jsonfrom django.contrib.auth import authenticate, login, logout
from django.http import JsonResponse
from django.shortcuts import render# Create your views here.
from django.views import Viewclass UserView(View):"""获取用户信息"""def get(self, request):if not request.user.is_authenticated:return JsonResponse({'code': 401,'message': '用户未登录'})return JsonResponse({'code': 200,'message': 'OK','data': {'username': request.user.username}})class SessionView(View):def post(self, request):"""用户登录"""# 客户端的请求体是 json 格式content_type = request.content_typeif 'application/json' not in content_type:return JsonResponse({'code': 400,'message': '非 json 格式'})else:data = eval(request.body.decode('utf-8'))username = data.get('username', '')password = data.get('password', '')user = authenticate(username=username,password=password)# 检查用户是否存在if not user:return JsonResponse({'code': 400,'message': '用户名或密码错误'})# 执行登录login(request, user)return JsonResponse({'code': 201,'message': 'OK'})def delete(self, request):"""退出登录"""logout(request)return JsonResponse({'code': 204,'message': 'OK'})

执行 python manage.py makemigrations、python manage.py migrate

2. 单元测试

Django 的单元测试使用的是 python unittest模块,在每个应用下面都有一个 tests.py 文件,将测试代码写入其中即可。如果测试的代码量比较多,我们需要将测试的代码分模块,那么可以在当前应用下创建 tests 包。

2.1 常用方法及使用事项

django 提供了 django.test.TestCase 单元测试基础类,它继承自 python 标准库中 unittest.TestCase

通常测试代码中自定义的类继承 TestCase 类,里面测试方法必须以 test_ 开头,一个类可以包含多个测试方法,如测试登录可以取名为 test_login

两个特殊方法

  • setUp(self):在每个测试方法执行之前调用,一般用于做一些准备工作
  • tearDown(self):在每个测试方法执行之后被调用,一般用来做一些清理工作

两个特殊类方法

  • setUpClass(cls):用于做类级别的准备工作,会在测试之前被调用,且一个类中,只能被调用一次
  • tearDown(self):用于做类级别的准备工作,会在测试之后被调用,且一个类中,只能被调用一次

客户端类 Client

客户端类用于模拟客户端发起 get、post、delet 等请求,且能自动保存 cookieDjango 的客户端类由 django.test.client.Client 提供。

另外 Client 还提供了 login 方法,可以很方便地进行用户登录。

注意:通过 client 发起请求时,URL 路径不用添加 schema://domain 前缀

2.2 如何执行

  • 测试所有类(多个应用/多个测试文件):python manage.py test
  • 测试具体应用、文件、类、方法:python manage.py test [app_name] [.test_file_name] [.class_name] [.test_method_name]

示例:

python manage.py test users
python manage.py test users.tests
python manage.py test users.tests.UserTestCase
python manage.py test users.tests.UserTestCase.test_user

测试时会创建自动创建测试数据库,测试完毕后也会自动销毁:

(MxShop) F:\My Projects\django_example_untest>python manage.py test users.tests.UserTestCase.test_user
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.241sOK
Destroying test database for alias 'default'...

看到 OK 两字表示测试用例通过,否则测试失败,还需检查程序代码哪里有问题。

2.3 测试代码

users/tests.py 编辑如下:

from django.contrib.auth.models import User
from django.test import TestCase
from django.test.client import Client# Create your tests here.
class UserTestCase(TestCase):def setUp(self):# 创建测试用户self.username = 'rose'self.password = 'rose123'self.user = User.objects.create_user(username=self.username, password=self.password)# 实例化 client 对象self.client = Client()# 登录self.client.login(username=self.username, password=self.password)def tearDown(self):"""删除测试用户"""self.user.delete()def test_user(self):"""测试获取用户信息接口"""path = '/users'resp = self.client.get(path)result = resp.json()self.assertEqual(result['code'], 200, result['message'])class SessionTestCase(TestCase):@classmethoddef setUpClass(cls):"""测试之前被调用,只调用一次"""# 创建测试用户cls.username = 'lila'cls.password = 'lila123'cls.user = User.objects.create_user(username=cls.username, password=cls.password)# 实例化 client 对象cls.client = Client()@classmethoddef tearDownClass(cls):"""测试之后调用,只调用一次,删除测试用户"""cls.user.delete()def test_login(self):"""测试登录接口"""path = '/session'auth_data = {"username": self.username,"password": self.password}# 将请求体设置为 jsonresp = self.client.post(path, data=auth_data, content_type='application/json')# 转换为 字典result = resp.json()# 检查登录结果self.assertEqual(result['code'], 201, result['message'])def test_logout(self):"""测试退出接口"""path = '/session'resp = self.client.delete(path)result = resp.json()# 断言,测试 result['code'] 是否为 204,若为 204 则表示测试通过self.assertEqual(result['code'], 204, result['message'])

测试:

(MxShop) F:\My Projects\django_example_untest>python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
...
----------------------------------------------------------------------
Ran 3 tests in 0.579sOK
Destroying test database for alias 'default'...

以上表示测试用例通过

2.4 RequestFactory 模拟请求

2.4.1 测试代码

上面测试代码,我们都是通过 client 来模拟请求,该请求最终会通过路由找到视图,并调用视图函数。

那么可不可以直接调用视图函数,而不通过 client,答案是可以的;Django 提供了一个 RequestFactory 对象可以直接调用视图函数 django.test.test.client.RequestFactory

users/tests.py 中新增代码:

from django.contrib.auth.models import User
from django.contrib.sessions.middleware import SessionMiddleware
from django.test import TestCase
from django.test.client import Client, RequestFactoryclass SessionRequestFactoryTestCase(TestCase):"""RequestFactory 直接调用视图函数"""@classmethoddef setUpClass(cls):cls.username = 'john'cls.password = 'john123'cls.user = User.objects.create_user(username=cls.username, password=cls.password)@classmethoddef tearDownClass(cls):"""删除测试用户"""cls.user.delete()def test_login(self):"""测试登录视图函数"""request_factory = RequestFactory()path = '/session'auth_data = {'username': self.username,'password': self.password}# 构建请求对象request = request_factory.post(path, data=auth_data, content_type='application/json')# 登录视图函数login_function = SessionView().post# 调用视图函数resp = login_function(request)print(resp.content)def test_logout(self):"""测试退出函数"""request_factory = RequestFactory()path = '/session'request = request_factory.delete(path)# 退出视图函数logout_function = SessionView().delete# 调用视图resp = logout_function(request)print(resp.content)

测试:

python manage.py testAttributeError: 'WSGIRequest' object has no attribute 'session'

提示没有 session 属性,这是因为 login、logout 两个函数都有利用 session 对用户信息进行操作(如:获取用户个人信息、清除个人信息等),查看相关源码可获知:

def login(request, user, backend=None):"""Persist a user id and a backend in the request. This way a user doesn'thave to reauthenticate on every request. Note that data set duringthe anonymous session is retained when the user logs in."""session_auth_hash = ''if user is None:user = request.userif hasattr(user, 'get_session_auth_hash'):session_auth_hash = user.get_session_auth_hash()if SESSION_KEY in request.session:if _get_user_session_key(request) != user.pk or (session_auth_hash andnot constant_time_compare(request.session.get(HASH_SESSION_KEY, ''), session_auth_hash)):# To avoid reusing another user's session, create a new, empty# session if the existing session corresponds to a different# authenticated user.request.session.flush()else:request.session.cycle_key()try:backend = backend or user.backendexcept AttributeError:backends = _get_backends(return_tuples=True)if len(backends) == 1:_, backend = backends[0]else:raise ValueError('You have multiple authentication backends configured and ''therefore must provide the `backend` argument or set the ''`backend` attribute on the user.')# 将相关信息存储到 session 中request.session[SESSION_KEY] = user._meta.pk.value_to_string(user)request.session[BACKEND_SESSION_KEY] = backendrequest.session[HASH_SESSION_KEY] = session_auth_hashif hasattr(request, 'user'):request.user = userrotate_token(request)user_logged_in.send(sender=user.__class__, request=request, user=user)def logout(request):"""Remove the authenticated user's ID from the request and flush their sessiondata."""# Dispatch the signal before the user is logged out so the receivers have a# chance to find out *who* logged out.user = getattr(request, 'user', None)if hasattr(user, 'is_authenticated') and not user.is_authenticated:user = Noneuser_logged_out.send(sender=user.__class__, request=request, user=user)# remember language choice saved to sessionlanguage = request.session.get(LANGUAGE_SESSION_KEY)# 清除 session request.session.flush()     if language is not None:request.session[LANGUAGE_SESSION_KEY] = languageif hasattr(request, 'user'):from django.contrib.auth.models import AnonymousUserrequest.user = AnonymousUser()

2.4.2 SessionMiddleware 源码

Django 是通过 SessionMiddleware 中间件 process_request 方法来实现的,具体可见源码:

# middleware.py
class SessionMiddleware(MiddlewareMixin):def __init__(self, get_response=None):self.get_response = get_responseengine = import_module(settings.SESSION_ENGINE)self.SessionStore = engine.SessionStoredef process_request(self, request):session_key = request.COOKIES.get(settings.SESSION_COOKIE_NAME)# 新增 session 属性(request 对象中)request.session = self.SessionStore(session_key)

2.4.3 解决方法

在上面 SessionRequestFactoryTestCase 测试类中我们只创建了请求对象 request,但是并没有添加 session 属性,所以导致后面的测试用例无法通过。

为此可以通过调用 SessionMiddleware.process_request 方法来设置 session 属性。

class SessionRequestFactoryTestCase(TestCase):....def test_login(self):"""测试登录视图函数"""request_factory = RequestFactory()path = '/session'auth_data = {'username': self.username,'password': self.password}# 构建请求对象request = request_factory.post(path, data=auth_data, content_type='application/json')# 调用中间件处理session_middleware = SessionMiddleware()session_middleware.process_request(request)# 登录视图函数login_function = SessionView().post# 调用视图函数resp = login_function(request)print(resp.content)def test_logout(self):"""测试退出函数"""request_factory = RequestFactory()path = '/session'request = request_factory.delete(path)# 调用中间件处理session_middleware = SessionMiddleware()session_middleware.process_request(request)# 退出视图函数logout_function = SessionView().delete# 调用视图resp = logout_function(request)print(resp.content)

再进行测试:

(MxShop) F:\My Projects\django_example_untest>python manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 5 tests in 0.676sOK
Destroying test database for alias 'default'...

2.5 总结

一个完整的 Django 生命周期:创建 request 对象 —> 执行中间层处理 —> 路由匹配 —> 视图处理 ----> 中间层处理 ----> 返回响应对象。

Django 模拟客户端发送请求的两种方式:

  • Client 对象:本质也是继承 RequestFactory 对象,调用 request 方法来发起完整的请求: 创建 request 对象–>执行中间层处理–>视图函数处理–>中间层处理–>返回响应对象。
  • RequestFactory 对象:request 对象就只做一件事,就是创建 request 对象,因此需要手动实现其他流程

注意:实际工作中一般采用 Client 对象,更为方便,只有遇到请求对象比较特殊或执行流程较复杂时,才通过 RequestFactory 对象来实现。

3. 代码测速覆盖度

3.1 快速上手

Coverage 是一个用来测试 python 程序代码覆盖率的功劳,可以识别代码的哪些部分已经被执行,有哪些可以执行但为执行的代码,可以用来衡量测试的有效性和完善性。

1、安装:

pip install coverage

2、配置:

项目根目录新建 .coveragerc 文件:

[run]
branch = True   # 是否统计条件语句分支覆盖情况,if 条件语句中的判断通常有 True 和 False,设置为 TRUE 是,会测量这两种情况是否都被测试到
source = .  # 指定需统计的源代码目录,这里为当前目录,即项目根目录[report]
show_missing = True # 在生成的统计报告中显示未被测试覆盖到的代码行号

coverage 遵循 ini 配置语法,上面有两个配置块,每个配置块表示不同的含义。

3、运行:

清除上一次的统计信息:coverage erase

coverage run 来代替 python manage.py test 来测试:

(MxShop) F:\My Projects\django_example_untest>coverage run manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
....
----------------------------------------------------------------------
Ran 5 tests in 0.745sOK
Destroying test database for alias 'default'...

生成覆盖率统计报告(查看已覆盖数、未覆盖数及覆盖百分比):

(MxShop) F:\My Projects\django_example_untest>coverage report
Name                                Stmts   Miss Branch BrPart  Cover   Missing
-------------------------------------------------------------------------------
django_example_untest\__init__.py       0      0      0      0   100%
django_example_untest\settings.py      18      0      0      0   100%
django_example_untest\urls.py           4      0      0      0   100%
django_example_untest\wsgi.py           4      4      0      0     0%   10-16
manage.py                               9      2      2      1    73%   5->exit, 9-10
users\__init__.py                       0      0      0      0   100%
users\admin.py                          1      0      0      0   100%
users\apps.py                           3      0      0      0   100%
users\migrations\__init__.py            0      0      0      0   100%
users\models.py                         1      0      0      0   100%
users\tests.py                         64      0      0      0   100%
users\views.py                         26      3      6      3    81%   11->12, 12, 30->31, 31, 42->43, 43
-------------------------------------------------------------------------------
TOTAL                                 130      9      8      4    91%

注意:倒数第二列为测试覆盖率,倒数第一列为未覆盖的代码行号

获取更详细的信息,会在同级目录生成一个 htmlcov 文件夹,打开 index.html 文件即可查看测试覆盖情况:


生成 xml 格式的报告:

(MxShop) F:\My Projects\django_example_untest>coverage xml

更多命令可使用:coverage --help 查看

3.2 高级用法

查看上述的报告,可看到很多覆盖度 100% 的文件(通常为源码文件),有时我们并不关心这些报告,当文件很多的时候要想快速找到我们需要测试的文件就有点困难,因此我们要将一些不必要的文件排除掉,通过 [run] 配置的 omit 配置项即可实现:

[run]
branch = True
source = .
omit =          # 忽略一些非核心的项目文件(具体按照自己项目来配置)django_example_untest\__init__.pydjango_example_untest\settings.pydjango_example_untest\urls.pydjango_example_untest\wsgi.pydjango_example_untest\manage.pyusers\__init__.pyusers\admin.pyusers\apps.pyusers\migrations\__init__.pyusers\models.py*\migrations\*[report]
show_missing = True
skip_covered = True     # 指定统计报告中不显示 100%的文件

再重新进行统计:

(MxShop) F:\My Projects\django_example_untest>coverage run manage.py test
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
b'{"code": 201, "message": "OK"}'
.b'{"code": 204, "message": "OK"}'
....
----------------------------------------------------------------------
Ran 5 tests in 0.687sOK
Destroying test database for alias 'default'...(MxShop) F:\My Projects\django_example_untest>coverage report
Name                            Stmts   Miss Branch BrPart  Cover   Missing
---------------------------------------------------------------------------
django_example_untest\wsgi.py       4      4      0      0     0%   10-16
manage.py                           9      2      2      1    73%   5->exit, 9-10
users\views.py                     26      3      6      3    81%   11->12, 12, 30->31, 31, 42->43, 43
---------------------------------------------------------------------------
TOTAL                             103      9      8      4    88%

注意:除第一次统计外,后面的统计最好按照 coverage erase --> coverage run manage.py test —> coverage report/html 的顺序来统计,这样做的目的是避免上一次统计结果影响下一次统计

另外 coverage html 可生成更为详细的统计报告,coverage report 只能查看未覆盖的范围的行号,不够直观。但是 coverage html 生成的 index.html 通过浏览器打开,点击相关文件可以可视化查看具体的范围来查看代码的覆盖情况,更为直观。

参考文章

  • Django 单元测试:http://www.itheima.com/news/20200807/182220.html
  • 统计 Django 项目的测试覆盖率:https://blog.csdn.net/a419240016/article/details/104708147

相关内容

热门资讯

证监会再次修订《上市公司治理准... 7月25日晚上,证监会发布通知对《上市公司治理准则(修订征求意见稿)》公开征求意见。本次修订目的是为...
从“闭眼买”到“不敢信”,山姆... 蓝鲨导读:中国零售崛起 作者 | 张二河 编辑 | 卢旭成 山姆会员店,因上架了一款韩国品牌陷入舆论...
原创 欧... 近日,欧盟委员会突然宣布了一则令人震惊的消息:7月25日,该委员会正式对中国太阳能玻璃发起第二次双反...
7月28日财经早餐:欧美达成贸... 来源:市场资讯 汇通财经APP讯——周一(北京时间7月28日),现货黄金交投于3335.78美元/盎...
科技早报 | 贝索斯完成大规模... 贝索斯完成一轮大规模亚马逊股票出售,套现57亿美元 亚马逊公司当地时间7月25日提交至美国证券交易...
股市必读:紫光股份(00093... 截至2025年7月25日收盘,紫光股份(000938)报收于25.04元,上涨0.64%,换手率1....
15%!美国与欧盟达成贸易协议... 据央视新闻报道,当地时间7月27日,美国总统特朗普表示,美国已与欧盟达成贸易协议,对欧盟输美商品征收...
早新闻|央行4000亿元MLF... 宏观热点 央行、农业农村部印发《关于加强金融服务农村改革 推进乡村全面振兴的意见》 近日,中国人...
21专访|细胞存储,《繁花》爷... 21世纪经济报道记者 赵云帆 上海报道 “我是一个真正意义上的创业者”,年过古稀的瞿建国,在采访中如...
华熙生物赵燕谈胶原蛋白乱象:科... 21世纪经济报道记者雷晨 北京报道 近年来,重组胶原蛋白成为医美和护肤领域的热门概念,市场宣传中不乏...
富春染织完成董事会选举换届 开... 7月25日晚间,富春染织公告显示,当日,公司2025年第一次临时股东会和富春染织第四届第一次董事会在...
圣湘生物:两款产品取得医疗器械... 每经AI快讯,圣湘生物(SH 688289,收盘价:22.94元)7月27日晚间发布公告称,圣湘生物...
10年期国债收益率升至1.73... 近期债券市场出现显著调整,多重因素交织推动收益率持续上行。权益市场强势表现与大宗商品价格上涨形成合力...
当对手都在做下沉 蜜雪冰城旗下... [ 今年5月,蜜雪集团跟巴西签署40亿元人民币的采购意向大单,其中大多数是咖啡豆。 ] 当星巴克、瑞...
新手必看!股指期货交易规则基础... 股指期货交易规则,看似复杂抽象,实则与我们的日常生活有着奇妙的共通之处。它就像一场精心编排的生活交响...
王登发履新茅台技开公司“一把手... 一则微信公众号发布的信息,披露了茅台集团旗下的技术开发公司“一把手”已换人。 近日,南都湾财社-酒水...
特斯拉机器人V3量产版亮相!马... 快科技7月27日消息,特斯拉的Optimus人形机器人V3量产版终于要来了!马斯克在最近的财报电话会...
原创 中... 在金融全球化的浪潮中,中国资本市场始终勇立潮头,不断探索前行。7月26日,中国资本市场学会成立大会暨...
报告:我国经济增长保持韧性 下... 央广网北京7月27日消息(记者 樊瑞)近日,中国金融四十人论坛(CF40论坛)发布《2025年第二季...
超6300亿元!A股银行“分红... 7月25日,成都银行完成权益分派股权登记,将于7月28日发放现金红利,这标志着A股上市银行2024年...