Django: есть ли способ подсчитать SQL-запросы из модульного теста?

Я пытаюсь узнать количество запросов, выполненных служебной функцией. Я написал модульный тест для этой функции, и функция работает хорошо. Что я хотел бы сделать, так это отслеживать количество 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. Однако билет все еще открыт. Между тем, есть ли другой способ сделать это?


person Manoj Govindan    schedule 10.08.2009    source источник


Ответы (8)


Начиная с Django 1.3 существует assertNumQueries Доступно именно для этой цели.

person Mitar    schedule 11.10.2011
comment
если вам нужно AssertNumQueriesLess добро пожаловать на stackoverflow.com/a/59089020/1731460 - person pymen; 28.11.2019

Ответ Виная правильный, с одним небольшим дополнением.

Среда модульного тестирования 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
person Jarret Hardie    schedule 10.08.2009
comment
Спасибо. Переключение DEBUG — это именно то, что мне нужно было сделать. :) - person Manoj Govindan; 10.08.2009
comment
Дополнительное примечание: вы действительно должны обернуть свой тестовый код в блок try:, поместив settings.DEBUG = False в соответствующий блок finally:. Таким образом, ваши другие тесты не будут испорчены настройкой DEBUG, если этот не пройден. - person SmileyChris; 28.04.2010
comment
вы можете использовать connection.use_debug_cursor = True вместо settings.DEBUG = True. На мой взгляд это будет более локальное решение - person Oduvan; 24.06.2012
comment
почему бы вам просто не указать settings.DEBUG = True в методе setUp() и settings.DEBUg = False в методе tearDown()? - person stantonk; 25.10.2012
comment
Помещение DEBUG=True в setUp() и tearDown() было бы идеальным, если вы хотите, чтобы запросы регистрировались для каждого теста. Если вы предпочитаете запускать поведение БД, похожее на производственное, для некоторых тестов из запросов журнала только в определенных тестах, то изменение настроек лучше выполнять в самой тестовой функции. Все зависит от вашей цели тестирования, я полагаю. - person Jarret Hardie; 26.10.2012
comment
@Oduvan У меня не работает connection.use_debug_cursor = True с django 1.11.7, просто DEBUG=True . - person Arpad Horvath; 29.03.2018
comment
Это очень плохой способ изменить настройки (как намекает @SmileyChris), у Django есть целая куча способов временно изменить настройки, не загрязняя другие тесты (по крайней мере, декоратор @override_settings существует с Django 1.4) - person Izkata; 25.05.2018

Если вы используете 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')
person tvorog    schedule 05.06.2017

Если вы не хотите использовать 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

настройки БД

person Daniel Barton    schedule 25.08.2016
comment
CaptureQueriesContext — очень недооцененный обработчик тестового контекста. Вы можете копаться во всех вещах о том, что делал ORM и почему. - person GDorn; 07.02.2019

В современном 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, но этот способ более явный (хотя и более подробный), или вы можете использовать reset_queries в предложении try столько раз, сколько вам нужно для оценки запросов, начиная с 0.

person danius    schedule 29.08.2015

Если у вас 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)
person Vinay Sajip    schedule 10.08.2009
comment
Спасибо. Я пробовал, но как ни странно, len(connection.queries) оказывается равным Zero(!) до и после вызова функции. Я протестировал его после замены вызова функции прямым вызовом MyModel.objects.filter() и все равно не повезло. К вашему сведению, я использую Django 1.1. - person Manoj Govindan; 10.08.2009
comment
Обновление: механизм работает, если я выполняю функцию в интерактивном режиме с помощью iPython. Конечно, это противоречит базе данных разработки, а не временной тестовой базе данных. Связано ли несоответствие с тем, как Django выполняет тесты в транзакции? - person Manoj Govindan; 10.08.2009
comment
DEBUG по умолчанию имеет значение False в тестах django. Это потому, что вы хотите протестировать свою живую среду. - person DylanYoung; 12.10.2016
comment
или: with self.settings(DEBUG=True): ... num_queries = len(connection.queries) см. документы - person djvg; 13.07.2021

Вот рабочий прототип менеджера контекста с 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, если вы хотите красиво распечатать список фактических запросов на стандартный вывод

person pymen    schedule 28.11.2019

Если вы хотите использовать для этого декоратор, есть приятный смысл:

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()
person ncopiy    schedule 27.11.2019