67

我正在尝试找出实用程序函数执行的查询数量。我已经为此功能编写了一个单元测试,并且该功能运行良好。我想做的是跟踪函数执行的 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功能请求。但是票仍然是开放的。与此同时,还有其他方法可以解决这个问题吗?

4

8 回答 8

73

从 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()
于 2011-10-11T15:40:20.183 回答
47

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

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

无论配置文件中 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
于 2009-08-10T12:34:21.290 回答
23

如果您正在使用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')
于 2017-06-05T13:29:04.740 回答
9

如果您不想使用 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

数据库设置

于 2016-08-25T14:12:09.127 回答
8

在现代 Django (>=1.8) 中,这里有很好的记录(它也记录在 1.7 中),你有方法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 子句中执行,但这种方式更明确(虽然更冗长),或者您可以在 try 子句中多次使用reset_queries因为您需要评估从 0 开始计数的查询。

于 2015-08-29T13:20:10.370 回答
6

这是带有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)

您还可以提供确切的数据库usingverbose如果您想将实际查询的列表漂亮地打印到标准输出

于 2019-11-28T12:33:52.813 回答
4

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

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)
于 2009-08-10T11:25:33.357 回答
-1

如果您想为此使用装饰器,则有一个很好的要点

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()
于 2019-11-27T07:44:55.887 回答