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

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值