Django Testing: See traceback where wrong Response

2020-04-17 07:04发布

This pattern is from the django docs:

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

From: https://docs.djangoproject.com/en/1.8/topics/testing/tools/#default-test-client

If the test fails, the error message does not help very much. For example if the status_code is 302, then I see 302 != 200.

The question is now: Where does the wrong HTTPResponse get created?

I would like to see the stacktrace of the interpreter where the wrong HTTPResponse object get created.

I read the docs for the assertions of django but found no matching method.

Update

This is a general question: How to see the wanted information immediately if the assertion fails? Since these assertions (self.assertEqual(response.status_code, 200)) are common, I don't want to start debugging.

Update 2016

I had the same idea again, found the current answer not 100% easy. I wrote a new answer, which has a simple to use solution (subclass of django web client): Django: assertEqual(response.status_code, 200): I want to see useful stack of functions calls

5条回答
The star\"
2楼-- · 2020-04-17 07:15

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)
查看更多
放荡不羁爱自由
3楼-- · 2020-04-17 07:23

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.

查看更多
萌系小妹纸
4楼-- · 2020-04-17 07:26

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.

查看更多
爷的心禁止访问
5楼-- · 2020-04-17 07:30

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)
查看更多
Anthone
6楼-- · 2020-04-17 07:34

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.

查看更多
登录 后发表回答