Django: is there a way to count SQL queries from an unit test?

前端 未结 8 914
广开言路
广开言路 2021-01-31 13:55

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

相关标签:
8条回答
  • 2021-01-31 14:22

    Here is the working prototype of context manager withAssertNumQueriesLessThen

    import json
    from contextlib import contextmanager
    from django.test.utils import CaptureQueriesContext
    from django.db import connections
    
    @contextmanager
    def withAssertNumQueriesLessThen(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.withAssertNumQueriesLessThen(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

    0 讨论(0)
  • 2021-01-31 14:23

    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.

    0 讨论(0)
  • 2021-01-31 14:28

    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()
    
    0 讨论(0)
  • 2021-01-31 14:30

    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
    
    0 讨论(0)
  • 2021-01-31 14:32

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

    0 讨论(0)
  • 2021-01-31 14:35

    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

    0 讨论(0)
提交回复
热议问题