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

相关内容

热门资讯

山西太钢不锈钢股份有限公司 2... 来源:证券日报 证券代码:000825 证券简称:太钢不锈 公告编号:2026-001 本公司及董...
把自己的银行贷款出借给别人,有... 新京报讯(记者张静姝 通讯员邸越洋)因贷款出借后未被归还,原告牛女士将被告杨甲、杨乙诉至法院,要求二...
金价暴跌,刚买的金饰能退吗?有... 黄金价格大跌,多品牌设置退货手续费。 在过去两三天,现货黄金价格经历了“过山车”般的行情,受金价下跌...
预计赚超2500万!“豆腐大王... 图片来源:图虫创意 在经历了一年亏损后,“豆腐大王”祖名股份(003030.SZ)成功实现扭亏为盈。...
特朗普提名“自己人”沃什执掌美... 据新华社报道,当地时间1月30日,美国总统特朗普通过社交媒体宣布,提名美国联邦储备委员会前理事凯文·...
爱芯元智将上市:连年大额亏损,... 撰稿|多客 来源|贝多商业&贝多财经 1月30日,爱芯元智半导体股份有限公司(下称“爱芯元智”,HK...
一夜之间,10只A股拉响警报:... 【导读】深康佳A等10家公司昨夜拉响退市警报 中国基金报记者 夏天 1月30日晚间,A股市场迎来一波...
谁在操控淳厚基金?左季庆为谁趟... 2026年1月6日,证监会一纸批复核准上海长宁国有资产经营投资有限公司(下称“长宁国资”)成为淳厚基...
工商银行党委副书记、行长刘珺会... 人民财讯1月31日电,1月29日,工商银行党委副书记、行长刘珺会见来访的上海电气集团党委书记、董事长...
布米普特拉北京投资基金管理有限... 从亚马逊到联合包裹,一场席卷美国企业的“瘦身”行动正在持续。多家企业近期承认,近年来的扩张步伐迈得过...
酒价内参1月31日价格发布 飞... 来源:酒业内参 新浪财经“酒价内参”过去24小时收集的数据显示,中国白酒市场十大单品的终端零售均价在...
筹码集中的绩优滞涨热门赛道股出... 2025年以来,在受多重因素的刺激下,科技、航天、基础化工等热门赛道中走出轮番上涨的结构性行情,其中...
2026年A股上市公司退市潮开... 来源:界面新闻 界面新闻记者 赵阳戈 随着2026年序幕拉开,A股市场新一轮“出清”即将上演。...
雷军官宣新直播:走进小米汽车工... 【太平洋科技快讯】1 月 31 日消息,小米创办人、董事长兼 CEO 雷军在社交媒体发文宣布,将于 ...
现货黄金直线跳水,跌破5200... 新闻荐读 1月29日晚,现货黄金白银快速走低,回吐盘中全部涨幅。23:15左右,现货黄金跌破5300...
加拿大拟与多国联合设立国防银行 新华社北京1月31日电 加拿大财政部长商鹏飞1月30日说,加拿大将在未来数月与国际伙伴密切合作,推进...
马斯克大消息!SpaceX申请... 据券商中国,美东时间1月30日,路透社报道,据两位知情人士透露,马斯克旗下SpaceX公司2025年...
澳网:雷巴金娜2-1萨巴伦卡女... 北京时间1月31日,2026赛季网球大满贯澳大利亚公开赛继续进行,在女单决赛中,5号种子雷巴金娜6-...
春节前白酒促销热:“扫码抽黄金... 春节临近,白酒市场再现价格异动。 近日,飞天茅台批价拉升,有酒商直言“年前要冲2000元关口”,引发...