Django:如何计算单元测试中的 SQL 查询?

问题描述 投票:0回答:8

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

django django-orm django-testing
8个回答
91
投票

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

49
投票

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
    

36
投票
如果您使用

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

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

数据库设置


8
投票
在现代 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子句上执行,但这种方式更明确(尽管更冗长),或者您可以在try子句中使用

reset_queries您需要评估从 0 开始计算的查询的次数。


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

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

using
verbose


4
投票
如果您在

DEBUG

 中将 
settings.py
 设置为 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)
    

-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()
    
© www.soinside.com 2019 - 2024. All rights reserved.