Django中间件/信号/单元测试小记
middleware文档参考
https://www.runoob.com/django/django-middleware.html
模块目录结构,自定义中间件通常放到模块middleware.py
post %ls
__init__.py __pycache__ admin.py apps.py middleware.py migrations models.py tests.py views.py
写个视图views.py
from django.shortcuts import render
from django.http import HttpResponse
def hello_django_bbs(request):
print('helloworld')
return HttpResponse('helloworld')
配置个url:urls.py
from post.views import hello_django_bbs
urlpatterns = [
......
path('hello', hello_django_bbs),
]
自定义中间件 middleware.py
from django.utils.deprecation import MiddlewareMixin
from django.http import JsonResponse
class FirstMiddleware(MiddlewareMixin):
def process_request(self, request):
print('FirstMiddleware: process_request')
def process_view(self, request, view_func, view_args, view_kwargs):
print('FirstMiddleware: process_view')
# 定义process_exception可以对视图函数抛出的异常做修复性工作
def process_exception(self, request, exception):
print('FirstMiddleware: process_exception')
return JsonResponse({'exception':str(exception)})
def process_response(self, request, response):
print('FirstMiddleware: process_response')
return response
class SecondMiddleware(MiddlewareMixin):
def process_request(self, request):
print('SecondMiddleware: process_request')
def process_view(self, request, view_func, view_args, view_kwargs):
print('SecondMiddleware: process_view')
def process_response(self, request, response):
print('SecondMiddleware: process_response')
return response
注册中间件settings.py
MIDDLEWARE = [
......
'post.middleware.FirstMiddleware',
'post.middleware.SecondMiddleware',
]
发现process_request是顺序执行的,process_response是倒序执行的
浏览器访问:http://127.0.0.1:8888/hello
FirstMiddleware: process_request # 顺序
SecondMiddleware: process_request
FirstMiddleware: process_view
SecondMiddleware: process_view
helloworld
SecondMiddleware: process_response # 倒序
FirstMiddleware: process_response
测试异常process_exception views.py
from django.shortcuts import render
from django.http import HttpResponse
# Create your views here.
def hello_django_bbs(request):
print('helloworld')
raise Exception('hello_django_bbs error') # 主动抛异常
return HttpResponse('helloworld')
FirstMiddleware: process_request
SecondMiddleware: process_request
FirstMiddleware: process_view
SecondMiddleware: process_view
helloworld
FirstMiddleware: process_exception # 当有异常会触发process_exception动作
SecondMiddleware: process_response
FirstMiddleware: process_response
Django信号机制(观察者模式-发布/订阅)
官网信号文档:https://docs.djangoproject.com/zh-hans/3.2/topics/signals/
允许若干个sender通知一组receiver某些操作已经发生了,receiver再去执行特定的动作。
信号包含三要素:
发送者:信号发出方
信号:信号本身
接收者:信号接受者
信号接收者本质上是一个简单的回调函数,将这个函数注册到信号上,当特定的事件发生时,发送者发送信号,回调函数被执行。
注:回调函数是同步执行,异步任务不能作为信号接收者
常用场景及案例
事件发生/完成的通知:使用信号可以降低代码耦合性。
事件发生后的清理/初始化:缓存等。
常用http请求、model 操作前后做处理,如下:
from django.http import HttpResponse
from django.core.signals import request_started, request_finished
from django.dispatch import receiver
# 编写http相关回调函数
# 法1:使用@receiver注册
@receiver(request_started, dispatch_uid="request_started")
def request_started_callback(sender, **kwargs):
print('request started: %s' % kwargs['environ'])
@receiver(request_finished)
def request_finished_callback(sender, **kwargs):
print('request finished: %s' % kwargs)
@receiver(got_request_exception)
def request_exception(sender, **kwargs):
print('request got_request_exception: %s' % kwargs)
# 法2:使用 signals对象 的 connect 方法注册。 dispatch_uid 可以避免同一个回调函数被重复执行
# request_started.connect(request_started_callback, dispatch_uid="request_started")
# request_finished.connect(request_finished_callback)
def hello_django_bbs(request):
print('helloworld')
# raise Exception('hello_django_bbs error')
return HttpResponse('helloworld')
"""
request started: {'PATH': 。。。。。name='<stderr>' mode='w' encoding='utf-8'>, 'wsgi.version': (1, 0), 'wsgi.run_once': False, 'wsgi.url_scheme': 'http', 'wsgi.multithread': True, 'wsgi.multiprocess': False, 'wsgi.file_wrapper': <class 'wsgiref.util.FileWrapper'>}
helloworld
[22/Jul/2022 15:00:36] "GET /hello?hahahah=hahahh HTTP/1.1" 200 10
request finished: {'signal': <django.dispatch.dispatcher.Signal object at 0x7fe56070f250>}
"""
from post.models import Comment
from django.db.models.signals import post_save
# 编写model回调函数
# post_save 模型实例保存后发送的信号:常用于缓存更新、消息通知、注册邮件发送等
@receiver(post_save, sender=Comment)
def comment_save_callback(sender, **kwargs):
print(sender, kwargs)
print('topic有了新评论')
"""
>>> from post.models import Comment
>>> from post import views
>>> comment = Comment.objects.get(id=1)
>>> comment.save() # 可以看到触发了signal
<class 'post.models.Comment'> {'signal': <django.db.models.signals.ModelSignal object at 0x7fb500bae040>, 'instance': <Comment: 1: gagaga>, 'created': False, 'update_fields': None, 'raw': False, 'using': 'default'}
topic有了新评论
"""
自定义信号
通常放到约定放到模块下的文件 signals.py
三步走:定义、注册、发送
import django.dispatch
from django.dispatch import receiver
"""
1、自定义信号signal
# 场景:新用户注册成功后发送邮件给用户,就可以利用信号机制处理
providing_args标识了信号发送时传递给回调函数的参数,request是HttpRequest实例,可以记录客户端信息,user是User实例,可以获取用户信息
"""
register_signal = django.dispatch.Signal(providing_args=["request", "user"])
"""
2、注册
"""
@receiver(register_signal, dispatch_uid="register_callback")
def register_callback(sender, **kwargs):
print("remote addr: %s, send mail to %s" % (kwargs['request'].META['REMOTE_ADDR'],kwargs['user'].email))
views.py
from django.http import HttpResponse
from post.signals import register_signal
def hello_django_bbs(request):
print('helloworld')
"""
3、发送模拟,随便放到一个方法下面
send 和 send_robust,它们会区别对待 receiver 可能抛出的异常,send 方法不会捕获任何由 receiver 抛出的异常,所以使用 send 方法不能保证所有的 receiver 都会得到信号通知。而 send_robust 则可以捕获抛出的异常,可以保证所有的 receiver 都接收到信号的通知。
"""
register_signal.send(hello_django_bbs, request=request, user=request.user)
return HttpResponse('helloworld')
"""output
helloworld
remote addr: 127.0.0.1, send mail to zhangzhidao@wisers.com
"""
单元测试
官网
django项目通常可以在各个模块的tests.py中编写单元测试,当项目大了不好维护可以考虑新建到test模块,分类放到对应文件。
单元测试测什么?
1、基础功能测试:逻辑功能/python语言级别的测试
2、模型测试:models增删改查
3、视图测试:对我接口的整体测试
单元测试编写
from django.test import TestCase
from post.models import Topic
from django.contrib.auth.models import User
from django.test import tag
"""
1、基础功能测试:逻辑功能/python语言级别的测试
2、模型测试:models增删改查
3、视图测试:对我接口的整体测试
"""
class SimpleTest(TestCase):
# 1、基础功能测试:逻辑功能 / python语言级别的测试
def test_add(self):
def add(x, y):
return x + y
self.assertEqual(add(1, 1), 2)
# 2、模型测试:models增删改查
@tag('major') # 针对指定tag测试:python manage.py test mysite.settings-test post.tests --tag=minor
def test_post_topic_model(self):
user = User.objects.create_user(username='username', password='password')
topic = Topic.objects.create(
title='test topic', content='test content', user=user
)
self.assertTrue(topic is not None)
self.assertEqual(Topic.objects.count(), 1)
topic.delete()
self.assertEqual(Topic.objects.count(), 0)
# 3、视图测试:对我接口的整体测试
@tag('minor')
def test_topic_detail_view(self):
user = User.objects.create_user(username='username', password='password')
topic = Topic.objects.create(
title='test topic', content='test content', user=user
)
response = self.client.get('/hello')
self.assertEqual(response.status_code, 200)
# response = self.client.get('/hello/%d/' % topic.id)
# self.assertEqual(response.json()['id'], topic.id)
"""output
%python manage.py test mysite.settings-test post.tests --tag=major
Creating test database for alias 'default'...
System check identified no issues (0 silenced).
.
----------------------------------------------------------------------
Ran 1 test in 0.150s
OK
Destroying test database for alias 'default'...
"""
默认情况下使用settings.py中配置的DB信息,并创建test数据库用于存储单元测试数据,可以通过指定配置文件,指定自己的DB,比如使用sqlite3来执行单元测试:python manage.py test mysite.settings-test
附:mysite/settings-test.py
......
import os
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': os.path.join(BASE_DIR, 'db.sqlite3')
}
}
......
单元测试覆盖率
可以使用coverage或sonarqube进行覆盖率检测
pip install coverage
# 保存coverage运行结果
coverage run --source='.' manage.py test mysite.settings-test post.tests
# 生成所有覆盖率报告
coverage report
# 跳过100%覆盖率的报告
coverage report --skip-coverd
# 以html展示覆盖率报告
coverage html --skip-covered