【问题标题】:Django: is there a way to count SQL queries from an unit test?Django:有没有办法从单元测试中计算 SQL 查询?
【发布时间】:2010-11-18 06:11:15
【问题描述】:

我试图找出一个实用函数执行的查询数。我已经为此功能编写了一个单元测试,并且该功能运行良好。我想做的是跟踪函数执行的 SQL 查询的数量,以便我可以查看在一些重构之后是否有任何改进。

def do_something_in_the_database():
    # Does something in the database
    # return result

class DoSomethingTests(django.test.TestCase):
    def test_function_returns_correct_values(self):
        self.assertEqual(n, <number of SQL queries executed>)

编辑:我发现为此有一个待处理的 Django feature request。但是票仍然是开放的。与此同时,还有其他方法可以解决这个问题吗?

【问题讨论】:

    标签: django django-orm django-testing


    【解决方案1】:

    如果您在settings.py 中将DEBUG 设置为True(可能在您的测试环境中如此),那么您可以按如下方式计算测试中执行的查询:

    from django.db import connection
    
    class DoSomethingTests(django.test.TestCase):
        def test_something_or_other(self):
            num_queries_old = len(connection.queries)
            do_something_in_the_database()
            num_queries_new = len(connection.queries)
            self.assertEqual(n, num_queries_new - num_queries_old)
    

    【讨论】:

    • 谢谢。我试过了,但奇怪的是 len(connection.queries) 在调用函数之前和之后都变成了零(!)。在用对 MyModel.objects.filter() 的直接调用替换函数调用后,我对其进行了测试,但仍然没有运气。仅供参考,我使用的是 Django 1.1。
    • 更新:如果我使用 iPython 以交互方式执行函数,则该机制有效。当然,这是针对开发数据库而不是瞬态测试数据库的。这种差异是否与 Django 在事务中执行测试的方式有关?
    • DEBUG 在 django 测试中默认设置为 False。这是因为您想测试您的 live 环境。
    • 或:with self.settings(DEBUG=True): ... num_queries = len(connection.queries)see docs
    【解决方案2】:

    Vinay 的回答是正确的,只是添加了一点点。

    Django 的单元测试框架在运行时实际上将 DEBUG 设置为 False,因此无论您在 settings.py 中有什么,除非您重新启用调试模式,否则您的单元测试中不会在 connection.queries 中填充任何内容。 Django 文档将rationale for this 解释为:

    无论配置文件中 DEBUG 设置的值如何,所有 Django 测试都以 DEBUG=False 运行。这是为了确保观察到的代码输出与生产环境中的输出相匹配。

    如果您确定启用调试不会影响您的测试(例如,如果您专门测试 DB 命中,听起来好像是这样),解决方案是在您的单元测试中临时重新启用调试,然后再把它放回去:

    def test_myself(self):
        from django.conf import settings
        from django.db import connection
    
        settings.DEBUG = True
        connection.queries = []
    
        # Test code as normal
        self.assert_(connection.queries)
    
        settings.DEBUG = False
    

    【讨论】:

    • 谢谢。切换调试正是我需要做的。 :)
    • 附加说明:您应该真正将测试代码包装在 try: 块中,将 settings.DEBUG = False 放入相应的 finally: 块中。这样,如果此测试失败,您的其他测试将不会被 DEBUG 设置“污染”。
    • 您可以使用 connection.use_debug_cursor = True 而不是 settings.DEBUG = True。我认为这将是更本地化的解决方案
    • 为什么不把 settings.DEBUG = True 放在 setUp() 方法中,把 settings.DEBUg = False 放在 tearDown() 方法中?
    • 这是一种非常糟糕的更改设置的方法(正如@SmileyChris 所暗示的那样),Django 有 a whole bunch of ways to temporarily change settings without contaminating the other tests(至少 @override_settings 装饰器自 Django 1.4 以来就存在)
    【解决方案3】:

    从 Django 1.3 开始,有一个 assertNumQueries 可用于此目的。

    使用它的一种方法(从 Django 3.2 开始)是作为上下文管理器:

    # measure queries of some_func and some_func2
    with self.assertNumQueries(2):
        result = some_func()
        result2 = some_func2()
    

    【讨论】:

    【解决方案4】:

    在现代 Django (>=1.8) 中,它有很好的记录(1.7 也有记录)here,你有方法 reset_queries 而不是分配 connection.queries=[] 这确实引发了一个错误,类似的东西适用于 django>=1.8:

    class QueriesTests(django.test.TestCase):
        def test_queries(self):
            from django.conf import settings
            from django.db import connection, reset_queries
    
            try:
                settings.DEBUG = True
                # [... your ORM code ...]
                self.assertEquals(len(connection.queries), num_of_expected_queries)
            finally:
                settings.DEBUG = False
                reset_queries()
    

    您也可以考虑在 setUp/tearDown 上重置查询,以确保为每个测试重置查询,而不是在 finally 子句中进行,但这种方式更明确(虽然更冗长),或者您可以使用 reset_queries 在 try 子句中根据需要计算从 0 开始计数的查询次数。

    【讨论】:

      【解决方案5】:

      如果您不想使用 TestCase(使用 assertNumQueries)或将设置更改为 DEBUG=True,您可以使用上下文管理器 CaptureQueriesContext(与使用 assertNumQueries 相同)。

      from django.db import ConnectionHandler
      from django.test.utils import CaptureQueriesContext
      
      DB_NAME = "default"  # name of db configured in settings you want to use - "default" is standard
      connection = ConnectionHandler()[DB_NAME]
      with CaptureQueriesContext(connection) as context:
          ... # do your thing
      num_queries = context.initial_queries - context.final_queries
      assert num_queries == expected_num_queries
      

      db settings

      【讨论】:

      • CaptureQueriesContext 是一个被严重低估的测试上下文处理程序。您可以深入了解有关 ORM 做了什么以及为什么做的各种事情。
      【解决方案6】:

      如果你使用pytestpytest-djangodjango_assert_num_queries 用于此目的:

      def test_queries(django_assert_num_queries):
          with django_assert_num_queries(3):
              Item.objects.create('foo')
              Item.objects.create('bar')
              Item.objects.create('baz')
      

      【讨论】:

        【解决方案7】:

        如果你想为此使用装饰器,有一个nice gist

        import functools
        import sys
        import re
        from django.conf import settings
        from django.db import connection
        
        def shrink_select(sql):
            return re.sub("^SELECT(.+)FROM", "SELECT .. FROM", sql)
        
        def shrink_update(sql):
            return re.sub("SET(.+)WHERE", "SET .. WHERE", sql)
        
        def shrink_insert(sql):
            return re.sub("\((.+)\)", "(..)", sql)
        
        def shrink_sql(sql):
            return shrink_update(shrink_insert(shrink_select(sql)))
        
        def _err_msg(num, expected_num, verbose, func=None):
            func_name = "%s:" % func.__name__ if func else ""
            msg = "%s Expected number of queries is %d, actual number is %d.\n" % (func_name, expected_num, num,)
            if verbose > 0:
                queries = [query['sql'] for query in connection.queries[-num:]]
                if verbose == 1:
                    queries = [shrink_sql(sql) for sql in queries]
                msg += "== Queries == \n" +"\n".join(queries)
            return msg
        
        
        def assertNumQueries(expected_num, verbose=1):
        
            class DecoratorOrContextManager(object):
                def __call__(self, func):  # decorator
                    @functools.wraps(func)
                    def inner(*args, **kwargs):
                        handled = False
                        try:
                            self.__enter__()
                            return func(*args, **kwargs)
                        except:
                            self.__exit__(*sys.exc_info())
                            handled = True
                            raise
                        finally:
                            if not handled:
                                self.__exit__(None, None, None)
                    return inner
        
                def __enter__(self):
                    self.old_debug = settings.DEBUG
                    self.old_query_count = len(connection.queries)
                    settings.DEBUG = True
        
                def __exit__(self, type, value, traceback):
                    if not type:
                        num = len(connection.queries) - self.old_query_count
                        assert expected_num == num, _err_msg(num, expected_num, verbose)
                    settings.DEBUG = self.old_debug
        
            return DecoratorOrContextManager()
        

        【讨论】:

          【解决方案8】:

          这是 带有AssertNumQueriesLessThan 的上下文管理器的工作原型

          import json
          from contextlib import contextmanager
          from django.test.utils import CaptureQueriesContext
          from django.db import connections
          
          @contextmanager
          def withAssertNumQueriesLessThan(self, value, using='default', verbose=False):
              with CaptureQueriesContext(connections[using]) as context:
                  yield   # your test will be run here
              if verbose:
                  msg = "\r\n%s" % json.dumps(context.captured_queries, indent=4)
              else:
                  msg = None
              self.assertLess(len(context.captured_queries), value, msg=msg)
          

          它可以简单地用于您的单元测试,例如检查每个 Django REST API 调用的查询数

              with self.withAssertNumQueriesLessThan(10):
                  response = self.client.get('contacts/')
                  self.assertEqual(response.status_code, 200)
          

          如果您想要将实际查询列表漂亮地打印到标准输出

          ,您还可以提供准确的 DB usingverbose

          【讨论】:

            猜你喜欢
            • 2020-02-16
            • 1970-01-01
            • 2021-08-23
            • 2018-02-24
            • 2020-09-08
            • 1970-01-01
            • 1970-01-01
            • 2018-06-02
            • 1970-01-01
            相关资源
            最近更新 更多