首页 > 解决方案 > Django Rest Framework APIClient 在测试期间未处理异常

问题描述

我正在测试一个应该在 Django 模型中引发 ValidationError 的 API 端点(请注意,该异常是 Django 异常,而不是 DRF,因为它在模型中)。

from rest_framework.test import APITestCase


class TestMyView(APITestCase):
    # ...
    def test_bad_request(self):
        # ...
        response = self.client.post(url, data)
        self.assertEqual(response.status_code, status.HTTP_400_BAD_REQUEST)

但是,我的测试出现异常而不是通过。它甚至没有失败获得 500 而不是 400,它根本没有到达那里。DRF 的 APIClient 不应该处理所有异常吗?我在网上搜索过,但一无所获。我读过 DRF 不处理 Django 的本机 ValidationError,但这仍然不能解释为什么我什至没有得到 500。知道我做错了什么吗?

完整的堆栈跟踪

E
======================================================================
ERROR: test_cannot_create_duplicate_email (organizations.api.tests.test_contacts.TestContactListCreateView)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/code/organizations/api/tests/test_contacts.py", line 98, in test_cannot_create_duplicate_email
    response = self.jsonapi_post(self.url(new_partnership), data)
  File "/code/config/tests/base.py", line 166, in jsonapi_post
    url, data=json.dumps(data), content_type=content_type)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 300, in post
    path, data=data, format=format, content_type=content_type, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 213, in post
    return self.generic('POST', path, data, content_type, **extra)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 238, in generic
    method, path, data, content_type, secure, **extra)
  File "/usr/local/lib/python3.7/site-packages/django/test/client.py", line 422, in generic
    return self.request(**r)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 289, in request
    return super(APIClient, self).request(**kwargs)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/test.py", line 241, in request
    request = super(APIRequestFactory, self).request(**kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/test/client.py", line 503, in request
    raise exc_value
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/exception.py", line 34, in inner
    response = get_response(request)
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 115, in _get_response
    response = self.process_exception_by_middleware(e, request)
  File "/usr/local/lib/python3.7/site-packages/django/core/handlers/base.py", line 113, in _get_response
    response = wrapped_callback(request, *callback_args, **callback_kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/decorators/csrf.py", line 54, in wrapped_view
    return view_func(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/views/generic/base.py", line 71, in view
    return self.dispatch(request, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 495, in dispatch
    response = self.handle_exception(exc)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 455, in handle_exception
    self.raise_uncaught_exception(exc)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 492, in dispatch
    response = handler(request, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/generics.py", line 244, in post
    return self.create(request, *args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/mixins.py", line 21, in create
    self.perform_create(serializer)
  File "/usr/local/lib/python3.7/site-packages/rest_framework/mixins.py", line 26, in perform_create
    serializer.save()
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 214, in save
    self.instance = self.create(validated_data)
  File "/code/organizations/api/serializers.py", line 441, in create
    'partnership': self.context['partnership']
  File "/usr/local/lib/python3.7/site-packages/rest_framework/serializers.py", line 943, in create
    instance = ModelClass._default_manager.create(**validated_data)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/manager.py", line 82, in manager_method
    return getattr(self.get_queryset(), name)(*args, **kwargs)
  File "/usr/local/lib/python3.7/site-packages/django/db/models/query.py", line 422, in create
    obj.save(force_insert=True, using=self.db)
  File "/code/organizations/models.py", line 278, in save
    self.full_clean()
  File "/usr/local/lib/python3.7/site-packages/django/db/models/base.py", line 1203, in full_clean
    raise ValidationError(errors)
django.core.exceptions.ValidationError: {'__all__': ['Supplier contact emails must be unique per organization.']}

标签: djangotestingdjango-rest-framework

解决方案


问题:DRF 的 APIClient 不应该处理所有异常吗?

:不。它是一个测试客户端,它不会处理任何未捕获的异常,这就是测试客户端的工作方式。测试客户端传播异常,以便在未捕获异常时测试失败并“崩溃”。您可以测试是否引发了异常并且未被捕获self.assertRaises

问题:当我引发 ValidationError 但未捕获异常时,APIView 应该返回 HTTP_400_BAD_REQUEST。

答案

您应该查看APIView 的源代码

在方法内部,在创建对象时dispatch()引发的所有异常都会被捕获并调用该方法。responsehandle_exception()

您的例外是ValidationError. 关键线路是:

exception_handler = self.get_exception_handler()
context = self.get_exception_handler_context()
response = exception_handler(exc, context)
if response is None:
    self.raise_uncaught_exception(exc)

如果你没有改变settings.EXCEPTION_HANDLER,你会得到默认的 DRF 异常处理程序,源代码在这里

如果处理Http404,PermissionDeniedAPIException. APIView本身实际上也处理AuthenticationFailedand 。NotAuthenticated但不是ValidationError。所以它会返回None,因此视图会引发您ValidationError的测试,从而停止您的测试。

您在回溯中看到:

File "/usr/local/lib/python3.7/site-packages/rest_framework/views.py", line 455, in handle_exception
    self.raise_uncaught_exception(exc)

您可以决定处理比 DRF 处理的默认异常更多的异常,您可以在自定义异常处理中阅读此内容。

编辑:您也可以raise rest_framework.exceptions.ValidationError代替标准的 Django ValidationError。这是一个APIException,因此将由 DRF 作为HTTP400_BAD_REQUEST. [1]

旁注:幸运的是,DRF 并没有捕捉到每一个异常!如果您的代码存在严重缺陷,您实际上希望您的代码“崩溃”并生成错误日志,并且您的服务器返回 HTTP 500。这就是这里发生的情况。如果这不是测试客户端,则响应将是 HTTP 500。

[1] https://github.com/encode/django-rest-framework/blob/3.9.0/rest_framework/exceptions.py#L142


推荐阅读