2

考虑到我有一个简单的 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

仅当测试模块在视图模块初始化之前初始化时,此答案才有效。AFAIK,这种加载在 Django 中是不可配置的。

4

4 回答 4

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
于 2021-04-23T18:29:43.613 回答
2

首先,您需要my_custom_decorator进入另一个模块,最好与您的views.py.

然后你需要:

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

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

  • 猴子修补装饰器

测试.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"})
于 2021-04-20T14:51:30.983 回答
0

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

装饰器.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

视图.py

from decorators import my_custom_decorator

class SomeAPIView(APIView):

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

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

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

于 2021-04-18T06:21:32.050 回答
-2

为简单起见,我认为最好将逻辑从装饰器拆分到其他地方。所以,我创建了一个名为_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"})
于 2021-04-24T04:34:01.810 回答