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

Man*_*dan 49 django django-orm django-testing

我试图找出实用程序函数执行的查询数.我已经为这个函数编写了一个单元测试,并且该函数运行良好.我想要做的是跟踪函数执行的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>)
Run Code Online (Sandbox Code Playgroud)

编辑:我发现有一个待定的Django 功能请求.但是票仍然是开放的.与此同时还有另一种方法可以解决这个问题吗?

Mit*_*tar 61

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


Jar*_*die 41

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
Run Code Online (Sandbox Code Playgroud)

  • 另外需要注意:您应该将测试代码包装在try:block中,将settings.DEBUG = False放在相应的finally:块中.这样,如果这个测试失败,你的其他测试不会被DEBUG设置"污染". (11认同)
  • 谢谢.切换DEBUG正是我需要做的.:) (3认同)
  • 您可以使用connection.use_debug_cursor = True而不是settings.DEBUG = True.在我看来,这将是更多的本地解决方案 (2认同)
  • 这是更改设置的一种非常糟糕的方式(正如@SmileyChris 所暗示的那样),Django 有 [一大堆方法可以在不影响其他测试的情况下临时更改设置](https://docs.djangoproject.com/en/dev /topics/testing/tools/#overriding-settings)(至少`@override_settings` 装饰器从 Django 1.4 开始就已经存在了) (2认同)

Dan*_*ton 8

如果您不想使用 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
Run Code Online (Sandbox Code Playgroud)

数据库设置

  • CaptureQueriesContext 是一个被严重低估的测试上下文处理程序。您可以深入了解 ORM 做了什么以及为什么做的各种事情。 (3认同)

pym*_*men 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)
Run Code Online (Sandbox Code Playgroud)

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

    with self.withAssertNumQueriesLessThan(10):
        response = self.client.get('contacts/')
        self.assertEqual(response.status_code, 200)
Run Code Online (Sandbox Code Playgroud)

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


dan*_*ius 7

在现代 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()
Run Code Online (Sandbox Code Playgroud)

您也可以考虑在 setUp/tearDown 上重置查询以确保为每个测试重置查询而不是在 finally 子句上执行此操作,但这种方式更明确(虽然更详细),或者您可以在 try 子句中多次使用reset_queries因为您需要评估从 0 开始计数的查询。


小智 7

如果您使用的pytestpytest-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')
Run Code Online (Sandbox Code Playgroud)