Gotchas

Writing tests

TL;DR

If:

  • you’ve customized the exception handler and,

  • the view raises an exception that causes a 5xx status code and,

  • you’re writing a test that ensures that the view will return the proper response when the exception is raised

Then, make sure to pass raise_request_exception=False, otherwise the test will keep failing. raise_request_exception=False allows returning a 500 response instead of raising an exception.

The long version

I faced this while writing a test for this package, so I wanted to share it in case someone else stumbles upon it. I was testing a custom exception formatter to make sure it’s used when set in settings and that the error response format matches my expectation. So, here’s the test

# views.py
from rest_framework.views import APIView

class ErrorView(APIView):
    def get(self, request, *args, **kwargs):
        raise Exception("Internal server error.")
# urls.py
from django.urls import path

from .views import ErrorView

urlpatterns = [
    path("error/", ErrorView.as_view()),
]
# tests.py
import pytest
from rest_framework.test import APIClient

from drf_standardized_errors.formatter import ExceptionFormatter
from drf_standardized_errors.types import ErrorResponse


@pytest.fixture
def api_client():
    return APIClient()


def test_custom_exception_formatter_class(settings, api_client):
    settings.DRF_STANDARDIZED_ERRORS = {
        "EXCEPTION_FORMATTER_CLASS": "tests.CustomExceptionFormatter"
    }
    response = api_client.get("/error/")
    assert response.status_code == 500
    assert response.data["type"] == "server_error"
    assert response.data["code"] == "error"
    assert response.data["message"] == "Internal server error."
    assert response.data["field_name"] is None


class CustomExceptionFormatter(ExceptionFormatter):
    def format_error_response(self, error_response: ErrorResponse):
        """return one error at a time and change error response key names"""
        error = error_response.errors[0]
        return {
            "type": error_response.type,
            "code": error.code,
            "message": error.detail,
            "field_name": error.attr,
        }

This test kept failing and showing a traceback including raise Exception("Internal server error."). To me, it seemed like the exception handler is not doing its job.

Running the test in debug mode, I was able to see that the response returned by the view is indeed what I expected, yet, the test is still failing.

Looking again at the test traceback and after reading the relevant code in django test client, that’s when I realized what’s going on: the test client defines a receiver for the signal got_request_exception and if that signal is sent, it concludes that an issue happened and raises the exception. In my test, I was raising an Exception("Internal server error.") that is considered a server error so, the signal is sent out by the exception handler and django fails the test since it receives the signal.

As for why is the signal sent out by the exception handler in the first place, that’s because error monitoring tools (like Sentry) rely on it to collect exception information and make it available through their UI. Also, and as found during the debugging of this issue, django test client needs it to determine if the view in question has raised an exception or not and notify the developer.

The nice thing is that django test client allows retrieving the response without raising the exception. That’s possible by passing raise_request_exception=False when instantiating the test client.