Django:有没有办法计算单元测试中的 SQL 查询数量?

发布于 2024-08-01 14:00:24 字数 533 浏览 8 评论 0原文

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

I am trying to find out the number of queries executed by a utility function. I have written a unit test for this function and the function is working well. What I would like to do is track the number of SQL queries executed by the function so that I can see if there is any improvement after some refactoring.

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>)

EDIT: I found out that there is a pending Django feature request for this. However the ticket is still open. In the meantime is there another way to go about this?

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(8

染年凉城似染瑾 2024-08-08 14:00:24

如果你想使用装饰器,有一个很好的要点

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()

If you want to use a decorator for that there is a 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()
檐上三寸雪 2024-08-08 14:00:24

如果您在 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)

If you have DEBUG set to True in your settings.py (presumably so in your test environment) then you can count queries executed in your test as follows:

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)
拿命拼未来 2024-08-08 14:00:24

这是上下文管理器 withAssertNumQueriesLessThan 的工作原型,

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提供精确的

Here is the working prototype of context manager withAssertNumQueriesLessThan

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)

It can be simply used in your unit tests for example for checking the number of queries per Django REST API call

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

Also you can provide exact DB using and verbose if you want to pretty-print list of actual queries to stdout

凉城已无爱 2024-08-08 14:00:24

在现代 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 子句上执行此操作,但这way 更明确(尽管更详细),或者您可以根据需要在 try 子句中使用 reset_queries 多次来评估从 0 开始计数的查询。

In modern Django (>=1.8) it's well documented (it's also documented for 1.7) here, you have the method reset_queries instead of assigning connection.queries=[] which indeed is raising an error, something like that works on 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()

You may also consider resetting queries on setUp/tearDown to ensure queries are reset for each test instead of doing it on finally clause, but this way is more explicit (although more verbose), or you can use reset_queries in the try clause as many times as you need to evaluate queries counting from 0.

征棹 2024-08-08 14:00:24

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

数据库设置

If you don't want use TestCase (with assertNumQueries) or change settings to DEBUG=True, you can use context manager CaptureQueriesContext (same as assertNumQueries using).

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

想念有你 2024-08-08 14:00:24

如果您使用 pytest,则 pytest-django 具有 django_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')

If you are using pytest, pytest-django has django_assert_num_queries fixture for this purpose:

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')
鼻尖触碰 2024-08-08 14:00:24

Vinay 的回答是正确的,但有一点补充。

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

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

如果您确定启用调试不会影响您的测试(例如,如果您专门测试数据库命中,就像听起来那样),解决方案是在单元测试中暂时重新启用调试,然后设置它之后回来:

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

Vinay's response is correct, with one minor addition.

Django's unit test framework actually sets DEBUG to False when it runs, so no matter what you have in settings.py, you will not have anything populated in connection.queries in your unit test unless you re-enable debug mode. The Django docs explain the rationale for this as:

Regardless of the value of the DEBUG setting in your configuration file, all Django tests run with DEBUG=False. This is to ensure that the observed output of your code matches what will be seen in a production setting.

If you're certain that enabling debug will not affect your tests (such as if you're specifically testing DB hits, as it sounds like you are), the solution is to temporarily re-enable debug in your unit test, then set it back afterward:

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
蓝天 2024-08-08 14:00:24

从 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()

Since Django 1.3 there is a assertNumQueries available exactly for this purpose.

One way to use it (as of Django 3.2) is as a context manager:

# measure queries of some_func and some_func2
with self.assertNumQueries(2):
    result = some_func()
    result2 = some_func2()
~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文