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

相关内容

热门资讯

企业IP打造指南:小公司低成本... 小公司做企业IP,不是为了装门面,而是让客户在没见到你之前,就能通过内容知道你是谁、你解决什么问题、...
官方:赵心童入选世界斯诺克名人... 北京时间5月8日消息,世界斯诺克巡回赛(WST)今日正式公布了2025/26赛季年终奖项及名人堂更新...
小灰熊AI学员王锋:希望能跟上... 35了,老程序员了。 从进入互联网行业到现在,其实已经做了很多年移动端开发。最早那几年,安卓行业发展...
原创 2... 2026年全国两会把稳定房地产市场列为重点工作,政府工作报告明确提出因城施策控增量、去库存、优供给。...
一年翻倍,六年未归——徽商银行... 文:向善财经 今年的港股市场,与A股市场出现了明显的分化。 A股这边,科技板块在AI浪潮中热闹非凡;...
古井贡酒2025:在行业深度调... 以“稳”为底、以“新”为翼。 文/每日财报 杜康 在行业库存高企、价格倒挂的背景下,当多数酒企在为...
好上好8408万收购鼎瑞芯加码... 5月7日晚,好上好(001298.SZ)抛出一份收购公告,拟以8408万元现金收购深圳市鼎瑞芯科技有...
全面大撤离!李嘉诚英国“套现”... 突发,李嘉诚又卖了。 这次,套现了455亿。 金额不少,但更值得关注的是透露着不同寻常的信号。 因为...
油气价格上涨加剧法国一季度贸易... 据新华社,法国海关7日发布的数据显示,受中东局势推高国际油气价格影响,法国今年第一季度贸易逆差扩大至...
昆仑芯启动科创板IPO上市辅导... 5月8日,据证监会官网显示,昆仑芯(北京)科技股份有限公司于2026年5月7日正式启动科创板上市辅导...
贵州茅台酒股份有限公司关于回购... 来源:上海证券报 证券代码:600519 证券简称:贵州茅台 公告编号:临2026-016 贵州茅...
百度昆仑芯启动科创板上市辅导,... 5月8日,证监会官网显示,昆仑芯(北京)科技股份有限公司 (下称“昆仑芯”)于2026年5月7日正式...
滕州信华的承压时刻:罚单、失信... 2026年4月末,滕州信华美元债单日跌近2%,关联方被列“老赖”。半年前,这家AA+城投曾因非市场化...
002808,或被终止上市! 【导读】因触及财务类退市指标,*ST恒久或被终止上市 中国基金报记者 李智 又一A股或被终止上市。 ...
院士团队掌舵,溧阳这家企业已完... 近日,溧阳天目先导电池材料科技有限公司(下称“天目先导”)官宣完成B轮融资,投资方包括知卓创新资本、...
工商银行全新推出“工盈研选”品... 深圳商报·读创客户端记者 詹钰叶 近日,工商银行重磅推出「工盈研选」基金销售服务品牌,以客户盈利为核...
和讯信息胡云龙:逼空走势,周五... 今天市场出现逼空走势,场内投资者因持有筹码而尤为受益。五一前布局的投资者当前收获颇丰。然而,随着上证...
今晚,油价上调! 4月21日国内成品油价格下调以来,国际市场原油价格剧烈震荡,前期大幅上涨后近日有所回落,本次调价的前...
南方东英旗下两倍做多海力士,成... 【导读】南方东英旗下两倍做多海力士,成为全球最大的个股杠杆及反向产品 中国基金报记者 伊万 人工智能...
原创 金... 黄金,这东西从古至今就没离开过中国人的生活。从老辈人压箱底的小黄鱼,到如今年轻人结婚绕不开的“三金”...