简体   繁体   English

如何在 Django 中模拟视图装饰器

[英]How to mock view decorator in Django

Consider that I have a simple APIView as below,考虑到我有一个简单的 APIView 如下,

from rest_framework.views import APIView
from rest_framework.response import Response


def my_custom_decorator(func):
    def wrap(view, request):
        if request.method.lower():
            raise ValueError("Just for testing")
        return func(view, request)

    return wrap


class SomeAPIView(APIView):

    @my_custom_decorator
    def post(self, request):
        return Response({"message": "Success"})

Note that the view function post(...) is wrapped by the decorator @my_custom_decorator .请注意,视图 function post(...)由装饰器@my_custom_decorator包裹。 Noe, I want to write the test for this API and I tried like this不,我想为这个 API 写测试,我试过这样

from rest_framework.test import APITestCase
from django.urls import reverse
from unittest.mock import patch


class TestSomeAPIView(APITestCase):

    @patch("sample.views.my_custom_decorator")
    def test_decorator(self, mock_my_custom_decorator):
        url = reverse("some-api-view")
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"message": "Success"})

This didn't mock the @my_custom_decorator properly and thus gave me an exception.这没有正确模拟@my_custom_decorator ,因此给了我一个例外。

Question : How can I mock the @my_custom_decorator to retrieve a successful response?问题:如何模拟@my_custom_decorator以检索成功的响应?

Notes笔记

Update - 1更新 - 1

This answer will work only if the test module gets initialized before the initialization of the view module.仅当测试模块在视图模块初始化之前初始化时,此答案才有效。 AFAIK, this kind of loading isn't configurable in Django. AFAIK,这种加载在 Django 中是不可配置的。

This isn't a guaranteed solution but depending on your needs it may work to rewrite your decorator with a helper function that contains the logic that needs to be mocked.这不是一个有保证的解决方案,但根据您的需要,它可能会使用包含需要模拟的逻辑的帮助程序 function 重写您的装饰器。

For example:例如:

from rest_framework.views import APIView
from rest_framework.response import Response

def some_check_or_other_response(view, request):
    if request.method.lower():
        raise ValueError("Just for testing")
    if some_other_condition:
        return Response({})
    

def my_custom_decorator(func):
    def wrap(view, request):
        short_circuit_response = some_check_or_other_response(view, request)
        if short_circuit_response:
            return short_circuit_response
        return func(view, request)

    return wrap


class SomeAPIView(APIView):

    @my_custom_decorator
    def post(self, request):
        return Response({"message": "Success"})

and then接着

class TestSomeAPIView(APITestCase):

    @patch("sample.views.some_check_or_other_response")
    def test_decorator(self, mock_some_check):
        mock_some_check.return_value = ... # short-circuit with a return value
        mock_some_check.side_effect = ValueError(...) # simulate an exception
        ... # etc

First, you will need to move my_custom_decorator into another module, preferably within the same package as your views.py .首先,您需要将my_custom_decorator移动到另一个模块中,最好在与您的views.py相同的 package 中。

Then you need to:然后你需要:

  • Clear the module import cache for sample.decorators , all modules within your app that import it, and your settings.ROOT_URLCONF清除sample.decorators的模块导入缓存、应用程序中导入它的所有模块以及您的settings.ROOT_URLCONF

  • Clear the url cache that django uses internally清除django内部使用的url缓存

  • Monkey patch the decorator猴子修补装饰器

tests.py :测试.py

import sys
from django.conf import settings
from django.urls import clear_url_caches

def clear_app_import_cache(app_name):
    modules = [key for key in sys.modules if key.startswith(app_name)]

    for module_name in modules:
        del sys.modules[module_name]
    
    try:
        del sys.modules[settings.ROOT_URLCONF]
    except KeyError:
        pass
    clear_url_caches()

class TestSomeAPIView(APITestCase):
    @classmethod
    def setUpClass(cls):
        clear_app_import_cache('sample')

        from sample import decorators
        decorators.my_custom_decorator = lambda method: method

        super().setUpClass()

    @classmethod
    def tearDownClass(cls):
        # Make sure the monkey patch doesn't affect tests outside of this class.  
        # Might not be necessary
        super().tearDownClass()
        clear_app_import_cache('sample')

    def test_decorator(self):
        url = reverse("some-api-view")
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"message": "Success"})

First you need to move the decorator to a different module to get a change to mock it.首先,您需要将装饰器移动到不同的模块以进行更改以模拟它。

decorators.py装饰器.py

def my_custom_decorator(func):
    def wrap(view, request):
        if request.method.lower():
            raise ValueError("Just for testing")
        return func(view, request)
    return wrap

views.py视图.py

from decorators import my_custom_decorator

class SomeAPIView(APIView):

    @my_custom_decorator
    def post(self, request):
        return Response({"message": "Success"})

In your tests patch the decorator before it get applied, like this在您的测试中,在应用之前修补装饰器,就像这样

tests.py测试.py

from unittest.mock import patch
patch("decorators.my_custom_decorator", lambda x: x).start()

from rest_framework.test import APITestCase

from django.urls import reverse


class TestSomeAPIView(APITestCase):

    def test_decorator(self):
        url = reverse("some-api-view")
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"message": "Success"})

For simplicity, I think it is better to split the logic from the decorator to somewhere else.为简单起见,我认为最好将逻辑从装饰器拆分到其他地方。 So, I created a function named _my_custom_decorator(...)所以,我创建了一个名为_my_custom_decorator(...)的 function

def _my_custom_decorator(func, view, request): # do most of the decorator logic here!!! if request.method.lower(): raise ValueError("Just for testing") return func(view, request)


def my_custom_decorator(func):
    def wrap(view, request):
        return _my_custom_decorator(func, view, request) # calling the newly created function
    return wrap


class SomeAPIView(APIView):
    @my_custom_decorator # this decorator remain unchanged!!!
    def post(self, request):
        return Response({"message": "Success"})

and now, mock the _my_custom_decorator(...) function in the tests,现在,在测试中模拟_my_custom_decorator(...) function,

def mock_my_custom_decorator(func, view, request): return func(view, request)


class TestSomeAPIView(APITestCase):

    @patch("sample.views._my_custom_decorator", mock_my_custom_decorator)
    def test_decorator(self):
        url = reverse("some-api-view")
        response = self.client.post(url)
        self.assertEqual(response.status_code, 200)
        self.assertEqual(response.json(), {"message": "Success"})

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM