Django如何在运行测试时查看SQL查询语句?

25

我的 Django 应用程序的一个单元测试失败,显示以下错误信息:

DatabaseError: ORA-00942: table or view does not exist

我希望能够查看导致该错误的实际 SQL 查询语句。你知道如何做到这一点吗?


这个有帮助吗?https://dev59.com/GnNA5IYBdhLWcg3wI6V4 - Darek
1
不完全是这样。我不想在测试用例中包含“print connection.queries”,因为要执行该行,我首先需要捕获异常。如果我捕获了该异常,测试将通过,这并不好。重新引发此异常并不是很优雅,我正在寻找更好的解决方案。 - mnowotka
另一件事是,'print' 在测试中不起作用 - 至少对我来说是这样... - mnowotka
无论如何,您都必须捕获异常才能在错误发生时显示任何信息。我认为重新引发异常并没有什么不雅之处--只需单独使用raise关键字即可通过并保持堆栈跟踪完整。 - Andrew Gorcester
哦,实际上,我想还有另一种解决方案——您可以记录 DEBUG 级别并配置记录器以在发生时将所有 SQL 查询写入日志。请参见 https://docs.djangoproject.com/en/dev/topics/logging/。 - Andrew Gorcester
在最近的cx_Oracle版本中,您可以将环境变量DPI_DEBUG_LEVEL设置为24,然后运行程序,参见cx_Oracle中的低级SQL跟踪。环境变量值显示在ODPI-C调试中。使用值16+8=24将提供SQL语句和错误信息。 - Christopher Jones
11个回答

35

5
这是最简单和最清晰的答案,因为它允许您捕捉任何任意小的代码部分的查询,如果您想要排除问题或优化查询,这正是您想要的。 - Octabode

19

如果您想从测试中打印/记录所有 SQL 查询,请尝试像这样子类化 TestCase

from django.conf import settings
from django.template import Template, Context
import sys
from django.db import connection
from django.test import TestCase

class LoggingTestCase(TestCase):

  @staticmethod
  def setUpClass():
    # The test runner sets DEBUG to False. Set to True to enable SQL logging.
    settings.DEBUG = True
    super(LoggingTestCase, LoggingTestCase).setUpClass()

  @staticmethod
  def tearDownClass():
    super(LoggingTestCase, LoggingTestCase).tearDownClass()

    time = sum([float(q['time']) for q in connection.queries])
    t = Template("{{count}} quer{{count|pluralize:\"y,ies\"}} in {{time}} seconds:\n\n{% for sql in sqllog %}[{{forloop.counter}}] {{sql.time}}s: {{sql.sql|safe}}{% if not forloop.last %}\n\n{% endif %}{% endfor %}")
    print >> sys.stderr, t.render(Context({'sqllog': connection.queries, 'count': len(connection.queries), 'time': time}))

    # Empty the query list between TestCases.    
    connection.queries = []

在您的测试中,使用LoggingTestCase代替TestCase作为基类。只需记得如果您覆盖了tearDownClass,则需要调用它。


你也应该调用super setUpClass。如果不这样做,会有一些东西丢失,例如fixture加载。 - arsenbonbon
@arsenbonbon 说得好,现在已经修复了。如果你给我点了踩,请考虑取消踩的操作,参见:http://stackoverflow.com/help/privileges/vote-down - tuomassalo
Django真的不允许您设置某些环境变量以打印出所有查询吗? - Andy
如何为测试覆盖设置。 - djvg

9
你还可以执行以下操作以获取查询(然后例如打印或在测试中评估它)。
实际上,现在你 不应该改变 django.conf.settings,因此我使用 override_settings
from django.db import connection, reset_queries
from django.test import override_settings, TransactionTestCase

class TransactionTests(TransactionTestCase):

    @override_settings(DEBUG=True)
    def test_sql(self):
        reset_queries()
        try:
            # Code that uses the ORM goes here
        except Exception as e:
            pass
        self.assertEqual(connection.queries, [])

TestCase 也可能是合适的,可以查看此 答案 中的差异。

有关 SQL 输出的详细信息,请参阅 Django 文档


6
另一个选项是在测试中使用connection.execute_wrapper(),如下所示:
from django.db import connection

def logger(execute, sql, params, many, context):
    print(sql, params)
    return execute(sql, params, many, context)

class GizmoTest(TestCase):

    def test_with_sql_logging(self):
        with connection.execute_wrapper(logger):
            code_that_uses_database()

测试过Django 2.2。


2

这不是最干净的解决方案,但是如果您只是想快速进行调试而不安装其他软件包,则可以查找django/db中的execute()方法。

对于Oracle,我猜它在:

django/db/backends/oracle/base.py,然后查找:

def execute

对于PostgreSQL,它位于:

django/db/backends/postgresql_psycopg2/base.py

在CursorWrapper中有一个execute()方法。

两者都捕获IntegrityError和DatabaseError,您可以在那里添加打印语句。

对于想要查看所有SQL查询的人,请在函数调用后立即放置打印语句。


1
这是对我有效的解决方案(Django 3.1):
from django.test import TestCase


class TestSomething(TestCase):
    @override_settings(DEBUG=True)
    def test_something(self):
        pass
    
    def tearDown(self):
        from django.db import connection
        for query in connection.queries:
            print(f"✅ {query['sql']}\n")

来源


1
在使用pytestpytest-django时,只需为其创建一个fixture即可。
@pytest.fixture
def debug_queries(db):
    """ Because pytest run tests with DEBUG=False
        the regular query logging will not work, use this fixture instead
    """
    from django.db import connection
    from django.test.utils import CaptureQueriesContext
    with CaptureQueriesContext(connection):
        yield connection

然后在你的测试中

@pytest.mark.django_db
def test__queries(debug_queries):
    # run your queries here

当然,您的日志配置应启用查询的日志记录,类似于这样:
LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'standard': {
            'format': '%(asctime)s - %(levelname)s - %(name)s - %(message)s',
        },
    },
    'handlers': {
        'default': {
            'level': 'DEBUG',
            'class': 'logging.StreamHandler',
            'formatter': 'standard',
            'stream': 'ext://sys.stdout',
        },
    },
    'loggers': {
        'django.db.backends': {
            'level': 'DEBUG',
            'handlers': ['default'],
            'propagate': False,
        },
    }
}

SQL查询没有被打印出来。 - Suor
这对我有用! 你知道是否可以使用pytest-django中的settings fixture来为测试设置记录器吗?我尝试使用它,但在这种情况下记录日志并没有起作用。 - oglop
@oglop,不确定您所说的“使用设置夹具”的意思。您究竟想要实现什么? - Eugene K
@EugeneK pytest-django 有一个设置夹具,可以覆盖设置 https://pytest-django.readthedocs.io/en/latest/helpers.html?highlight=fixtures#settings 我在想,也许我可以将其用作“debug_queries”的参数,这样就不需要在test.settings.py中添加日志配置,只需在需要时使用夹具即可。 - oglop
@oglop你可以在 debug_queries fixture 中始终将记录器级别设置为 DEBUG,无需破解设置。不要忘记在 fixture 退出时还原级别。 - Eugene K

0

你可以在设置中将控制台级别更改为DEBUG。这适用于Django 1.9。

LOGGING = {
...
'handlers': {
    'console': {
        'level': 'DEBUG',
        'class': 'logging.StreamHandler',
        'formatter': 'simple'
        },
    }
...
}

由于性能原因,仅在设置了settings.DEBUG为True时才启用SQL日志记录,无论安装了哪些日志级别或处理程序。 - pymen

0

这里的所有选项都太复杂了/太多事情可能会出错。这适用于 django >= 1.11+ <= 4.x [在 main 分支上测试过] (直到将来他们把它弄坏为止..)

它通过完全忽略 settings.DEBUG 并注入 CursorDebugWrapper 来始终使用它,从而记录运行的 SQL。

import inspect
from collections import deque
from contextlib import contextmanager
from unittest import mock

from django.db import connections
from django.db.backends import utils


@contextmanager
def print_queries(using="default"):
    """
    [debug] Prints out all the queries in real time

    To avoid messing with django's logging and get the SQL working inside
    tests where `DEBUG` can be set to `False`, this bypasses all that and
    goes straight for the kill.

    Example:

        class TestSomething(TestCase):
            def test_something(self):
                with print_queries():
                    Model.objects.create(a=1, b=2)

        def test_something():
            with print_queries():
                Model.objects.create(a=1, b=2)

    """

    def fake_maker(self, cursor):
        """
        Normally `make_cursor` uses `CursorWrapper` which does NOT debug.
        """
        return utils.CursorDebugWrapper(cursor, self)

    class Snitch(deque):
        """
        Modified version of `deque` that `print()`s out all the items inserted to it.
        """

        def append(self, item):
            current_frame = inspect.currentframe().f_back
            while True:
                info = inspect.getframeinfo(current_frame)
                if "/django/" in info.filename:
                    current_frame = current_frame.f_back
                    continue
                break

            print("*", item["sql"], item["time"], f"{info.filename}:{info.lineno}")
            return super().append(item)

    connection_ = connections[using]
    mock_maker = mock.patch("django.db.backends.base.base.BaseDatabaseWrapper.make_cursor", fake_maker)
    mock_deque = mock.patch.object(connection_, "queries_log", Snitch(maxlen=connection_.queries_log.maxlen))

    with mock_maker, mock_deque:
        yield

使用方法如下:

def test_whatever():
    ...
    with print_queries():
        Model.objects.create(a=1, b=2)  # any query here
    ...

输出看起来像:

* SELECT .. FROM "table" WHERE ... 0.001 /full/path/file.py:136
* SELECT .. FROM "table" WHERE ... 0.001 /full/path/file.py:245

会告诉你查询在代码中的哪个位置。


0
到目前为止,我发现的最佳解决方案是使用django-debugtoolbar提供的debugsqlshell自定义Django管理命令。

3
您能详细说明如何使用debugsqlshell命令来运行测试吗?这在django-debugtoolbar的文档中没有解释。 - gogognome
@gogognome,我认为mnowotka误解了问题。好吧,实际上,我刚刚发现他是TS,所以这里是-1个错误的接受答案。 - Eugene K

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接