【问题标题】:Django Testing: See traceback where wrong Response gets createdDjango 测试:查看创建错误响应的回溯
【发布时间】:2017-04-21 04:46:10
【问题描述】:

此模式来自 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 web 客户端的子类):Django: assertEqual(response.status_code, 200): I want to see useful stack of functions calls

【问题讨论】:

  • 你是什么意思,错误的反应? 404 告诉您 /customer/details/ 没有作为 URL 匹配,或者被调用的视图显式返回 404。
  • @DanielRoseman 是的,我是对的。状态码 404 不是很好的例子。但问题仍然存在。我将错误的状态码更改为 302。
  • 通常 302 表示您错过了 url 的尾部斜杠(例如,它从 /customer/details 重定向到 /customer/details/)。如果没有,您需要查看视图并查看它在哪里返回重定向。测试客户端没有响应的回溯,因为生成响应时没有错误;它只是具有与您预期的不同的状态代码。
  • 302 的另一个原因是您被重定向到登录页面。你可以通过打印response['Location']来检查。
  • 服务器是你的吗?也许您可以在调试模式下运行它并显示错误消息,然后如果代码不符合预期,请使用您的测试显示 URL 的输出。

标签: python django unit-testing http-response-codes


【解决方案1】:

我认为这可以通过创建一个 TestCase 子类来实现,该子类将猴子补丁 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)

【讨论】:

    【解决方案2】:

    如果断言在没有调试的情况下失败,我如何查看回溯

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

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

    【讨论】:

    • 如果可以使用 pdb(交互式),那么应该可以使用 mock 和自定义断言(非交互式)。
    【解决方案3】:

    也许这对你有用:

    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 模块记录它,或者将其添加为assert 方法的消息。如果没有调试器,一旦你assert,打印任何有用的东西(通常)为时已晚。

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

    【讨论】:

    • 我认为您的回答并不能解决我的问题。您的解决方案显示了视图的结果 (response.content)。我的需要:“我想查看创建错误 HTTPResponse 对象的解释器的堆栈跟踪。”。如果响应创建内部服务器错误,则您的解决方案有效。但是对于重定向和“权限被拒绝”也需要这个。
    • 这当然解决了我的“导致内部服务器错误的原因”问题。
    【解决方案4】:

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

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

    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)
    

    【讨论】:

      【解决方案5】:

      我将 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 响应,则会导致长异常。如果你不怕长异常,你很快就会发现问题的根源。这就是我想要的,我很高兴。

      学分

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

      【讨论】:

        猜你喜欢
        • 2020-12-12
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 2020-10-27
        • 2014-09-12
        • 1970-01-01
        • 2013-10-24
        相关资源
        最近更新 更多