Django 测试:查看创建错误响应的回溯

Django Testing: See traceback where wrong Response gets created

此模式来自 django 文档:

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

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

如果测试失败,错误消息并没有多大帮助。例如,如果 status_code 是 302,那么我会看到 302 != 200

现在的问题是:在哪里创建了错误的 HTTPResponse?

我想查看创建了错误的 HTTPResponse 对象的解释器堆栈跟踪。

我阅读了 assertions of django 的文档,但没有找到匹配的方法。

更新

这是一个普遍性的问题:如果断言失败,如何立即看到想要的信息?由于这些断言(self.assertEqual(response.status_code, 200))很常见,我不想开始调试。

2016 年更新

我又有了同样的想法,发现当前的答案不是 100% 容易的。我写了一个新答案,它有一个简单易用的解决方案(django 网络客户端的子类):Django: assertEqual(response.status_code, 200): I want to see useful stack of functions calls

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

如果断言失败,则没有回溯。 client.get() 并没有失败,它只是返回了与您预期不同的响应。

您可以使用 pdb 单步执行 client.get() 调用,看看它返回意外响应的原因。

我认为可以通过创建一个 TestCase 子类来实现,该子类使用 monkeypatches django.http.response.HttpResponseBase.__init__() 来记录堆栈跟踪并将其存储在 Response 对象上,然后编写一个 assertResponseCodeEquals(response, status_code=200) 方法打印存储的堆栈跟踪失败以显示创建 Response 的位置。

我自己真的可以使用一个解决方案,并且可能会考虑实施它。

更新: 这是一个 v1 实现,它可以使用一些改进(例如只打印堆栈跟踪的相关行)。

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)

也许这对你有用:

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)

使用 @override_settings 获得 DEBUG=True 将获得堆栈跟踪,就像您 运行 处于 DEBUG 模式的实例一样。

其次,为了提供响应的内容,您需要 print 或使用 logging 模块记录它,或者将其添加为 [=16] 的消息=] 方法。没有调试器,一旦你assert,打印任何有用的东西就太晚了(通常)。

您还可以配置 logging 并添加处理程序以将消息保存在内存中,并打印所有这些;在自定义断言方法或自定义测试运行程序中。

我受到@Fush 提出的解决方案的启发,但我的代码使用的是 assertRedirects,这是一个更长的方法,而且代码太多,无法复制而不会对自己感到难过。

我花了一些时间弄清楚如何为每个断言调用 super() 并想出了这个。我已经包含了 2 个示例断言方法 - 它们基本上都是相同的。也许一些聪明的灵魂可以想到一些元类魔法,它对所有以 'response' 作为第一个参数的方法都这样做。

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)

我将 django 网络客户端子类化,得到这个:

用法

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

实施

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)

结论

如果创建了带有错误状态代码的 HTTP 响应,则会导致长时间异常。如果您不害怕长异常,您会很快看到问题的根源。这就是我想要的,我很高兴。

学分

这是基于这个问题的其他答案。