【问题标题】:How to mock view decorator in Django如何在 Django 中模拟视图装饰器
【发布时间】:2021-07-12 15:29:35
【问题描述】:

假设我有一个简单的 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"})

请注意,视图函数post(...) 被装饰器@my_custom_decorator 包装。 不,我想为此 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"})

这没有正确模拟@my_custom_decorator,因此给了我一个例外。

问题:如何模拟@my_custom_decorator 以检索成功的响应?

注意事项

更新 - 1

This answer 仅在测试模块在视图模块初始化之前被初始化时才起作用。 AFAIK,这种加载在 Django 中是不可配置的。

【问题讨论】:

    标签: python django unit-testing django-rest-framework mocking


    【解决方案1】:

    首先,您需要将装饰器移动到不同的模块以进行更改以模拟它。

    decorators.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

    from decorators import my_custom_decorator
    
    class SomeAPIView(APIView):
    
        @my_custom_decorator
        def post(self, request):
            return Response({"message": "Success"})
    

    在你的测试中,在应用之前修补装饰器, 像这样

    tests.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"})
    
    

    【讨论】:

    • 这是可行的,前提是必须在视图模块之前初始化测试模块。是否可以控制模块加载?
    • 是和不是。如果你在声明范围内的任何地方使用 reverse() (简而言之,不是函数或方法的一部分),Django 正在加载作为其启动序列的一部分,Django 将加载 url 并因此加载视图。您可以通过assert 'myapp.decorators' not in sys.modules.keys(), "Oops, something loaded myapp.decorators" 快​​速追踪它。
    【解决方案2】:

    首先,您需要将 my_custom_decorator 移动到另一个模块中,最好与 views.py 在同一个包中。

    那么你需要:

    • 清除 sample.decorators 的模块导入缓存、您应用中导入它的所有模块以及您的 settings.ROOT_URLCONF

    • 清除django内部使用的url缓存

    • 猴子修补装饰器

    tests.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"})
    

    【讨论】:

      【解决方案3】:

      这不是一个有保证的解决方案,但根据您的需要,它可能会使用包含需要模拟的逻辑的辅助函数重写您的装饰器。

      例如:

      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"})
      

      然后

      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
      

      【讨论】:

      • 是的,这确实有效,我正在遵循这种方法(通过将主逻辑与顶级装饰器分离)。
      • 顺便说一句,我创建了this answer,它具有我当前的设置。介意看看它并提出一些建议吗?
      • 是的,我认为你的比我的干净。也让我想知道您是否可以将其进一步简化为 my_custom_decorator = lambda func: functools.partial(_my_custom_decorator, func) (或跳过 lambda 并使用正确的 def,但只是尝试在此处的评论中加入一条线)。
      【解决方案4】:

      为简单起见,我认为最好将逻辑从装饰器拆分到其他地方。所以,我创建了一个名为 _my_custom_decorator(...)

      的函数
      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"})

      现在,在测试中模拟 _my_custom_decorator(...) 函数,

      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"})

      【讨论】:

      • 我还没有研究过 SO 的所有礼仪和规则……但这似乎是提供赏金,让某人花时间回答,然后简单地重复他们的回答并接受你的自己的副本有点……不酷?
      • 我非常感谢花时间回答这个问题(或 SO 中的任何问题)的人,因为我知道它的感受。如果您仔细观察,我有 mentioned 这个特定答案表明我当前的设置/解决方法,它不是任何 OP 答案的"精确副本"。所以,我不认为这是this 答案的“重复”。顺便说一句,我承认我没有在 OP 中提到我的解决方法。
      • 另外,我可以接受这个问题的任何答案,甚至是我自己的答案(我看不出有任何问题)。此外,我肯定会接受任何以更好的方法解决 OP 问题的答案
      猜你喜欢
      • 2012-08-02
      • 2021-12-23
      • 2018-10-18
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2014-06-07
      • 2016-08-14
      相关资源
      最近更新 更多