Django Testing: See traceback where wrong Response gets created

橙三吉。 提交于 2019-12-02 00:48:53

I think it could be achieved by creating a TestCase subclass that monkeypatches django.http.response.HttpResponseBase.__init__() to record a stack trace and store it on the Response object, then writing an assertResponseCodeEquals(response, status_code=200) method that prints the stored stack trace on failure to show where the Response was created.

I could actually really use a solution for this myself, and might look at implementing it.

Update: Here's a v1 implementation, which could use some refinement (eg only printing relevant lines of the stack trace).

import mock
from traceback import extract_stack, format_list
from django.test.testcases import TestCase
from django.http.response import HttpResponseBase

orig_response_init = HttpResponseBase.__init__

def new_response_init(self, *args, **kwargs):
    orig_response_init(self, *args, **kwargs)
    self._init_stack = extract_stack()

class ResponseTracebackTestCase(TestCase):
    @classmethod
    def setUpClass(cls):
        cls.patcher = mock.patch.object(HttpResponseBase, '__init__', new_response_init)
        cls.patcher.start()

    @classmethod
    def tearDownClass(cls):
        cls.patcher.stop()

    def assertResponseCodeEquals(self, response, status_code=200):
        self.assertEqual(response.status_code, status_code,
            "Response code was '%s', expected '%s'" % (
                response.status_code, status_code,
            ) + '\n' + ''.join(format_list(response._init_stack))
        )

class MyTestCase(ResponseTracebackTestCase):
    def test_index_page_returns_200(self):
        response = self.client.get('/')
        self.assertResponseCodeEquals(response, 200)

How do I see the traceback if the assertion fails without debugging

If the assertion fails, there isn't a traceback. The client.get() hasn't failed, it just returned a different response than you were expecting.

You could use a pdb to step through the client.get() call, and see why it is returning the unexpected response.

I was inspired by the solution that @Fush proposed but my code was using assertRedirects which is a longer method and was a bit too much code to duplicate without feeling bad about myself.

I spent a bit of time figuring out how I could just call super() for each assert and came up with this. I've included 2 example assert methods - they would all basically be the same. Maybe some clever soul can think of some metaclass magic that does this for all methods that take 'response' as their first argument.

from bs4 import BeautifulSoup
from django.test.testcases import TestCase


class ResponseTracebackTestCase(TestCase):

    def _display_response_traceback(self, e, content):
        soup = BeautifulSoup(content)
        assert False, u'\n\nOriginal Traceback:\n\n{}'.format(
            soup.find("textarea", {"id": "traceback_area"}).text
        )

    def assertRedirects(self, response, *args, **kwargs):
        try:
            super(ResponseTracebackTestCase, self).assertRedirects(response, *args, **kwargs)
        except Exception as e:
            self._display_response_traceback(e, response.content)

    def assertContains(self, response, *args, **kwargs):
        try:
            super(ResponseTracebackTestCase, self).assertContains(response, *args, **kwargs)
        except Exception as e:
            self._display_response_traceback(e, response.content)

Maybe this could work for you:

class SimpleTest(unittest.TestCase):
    @override_settings(DEBUG=True)
    def test_details(self):
        client = Client()
        response = client.get('/customer/details/')
        self.assertEqual(response.status_code, 200, response.content)

Using @override_settings to have DEBUG=True will have the stacktrace just as if you were running an instance in DEBUG mode.

Secondly, in order to provide the content of the response, you need to either print it or log it using the logging module, or add it as your message for the assert method. Without a debugger, once you assert, it is too late to print anything useful (usually).

You can also configure logging and add a handler to save messages in memory, and print all of that; either in a custom assert method or in a custom test runner.

I subclassed the django web client, to get this:

Usage

def test_foo(self):
    ...
    MyClient().get(url, assert_status=200)

Implementation

from django.test import Client

class MyClient(Client):
    def generic(self, method, path, data='',
                content_type='application/octet-stream', secure=False,
                assert_status=None,
                **extra):
        if assert_status:
            return self.assert_status(assert_status, super(MyClient, self).generic, method, path, data, content_type, secure, **extra)
        return super(MyClient, self).generic(method, path, data, content_type, secure, **extra)

    @classmethod
    def assert_status(cls, status_code, method_pointer, *args, **kwargs):
        assert hasattr(method_pointer, '__call__'), 'Method pointer needed, looks like the result of a method call: %r' % (method_pointer)

        def new_init(self, *args, **kwargs):
            orig_response_init(self, *args, **kwargs)
            if not status_code == self.status_code:
                raise HTTPResponseStatusCodeAssertionError('should=%s is=%s' % (status_code, self.status_code))
        def reraise_exception(*args, **kwargs):
            raise

        with mock.patch('django.core.handlers.base.BaseHandler.handle_uncaught_exception', reraise_exception):
            with mock.patch.object(HttpResponseBase, '__init__', new_init):
                return method_pointer(*args, **kwargs)

Conclusion

This results in a long exception if a http response with a wrong status code was created. If you are not afraid of long exceptions, you see very fast the root of the problem. That's what I want, I am happy.

Credits

This was based on other answers of this question.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!