From f132019a2653d79ebd5d4fc3fcf54cc497cbf1ec Mon Sep 17 00:00:00 2001 From: andreas Date: Sat, 2 Dec 2017 10:04:46 +0100 Subject: [PATCH 1/3] Add fixture for asserting maximum number of database queries --- docs/helpers.rst | 18 +++++++++++++++ pytest_django/fixtures.py | 48 +++++++++++++++++++++++++-------------- pytest_django/plugin.py | 1 + tests/test_fixtures.py | 16 ++++++++++++- 4 files changed, 65 insertions(+), 18 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 9d8329f45..1219f6746 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -265,6 +265,24 @@ Example Item.objects.create('baz') +``django_assert_max_num_queries`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This fixture allows to check for an expected maximum number of DB queries. +It currently only supports the default database. + + +Example +""""""" + +:: + + def test_max_queries(django_assert_max_num_queries): + with django_assert_max_num_queries(3): + Item.objects.create('foo') + Item.objects.create('bar') + + ``mailoutbox`` ~~~~~~~~~~~~~~ diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 5434a0dff..823edbffb 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -3,6 +3,7 @@ from __future__ import with_statement import os +from functools import partial import pytest @@ -17,7 +18,8 @@ __all__ = ['django_db_setup', 'db', 'transactional_db', 'admin_user', 'django_user_model', 'django_username_field', 'client', 'admin_client', 'rf', 'settings', 'live_server', - '_live_server_helper', 'django_assert_num_queries'] + '_live_server_helper', 'django_assert_num_queries', + 'django_assert_max_num_queries'] @pytest.fixture(scope='session') @@ -352,22 +354,34 @@ def _live_server_helper(request): request.addfinalizer(live_server._live_server_modified_settings.disable) -@pytest.fixture(scope='function') -def django_assert_num_queries(pytestconfig): +@contextmanager +def _assert_num_queries(config, num, exact=True): from django.db import connection from django.test.utils import CaptureQueriesContext + verbose = config.getoption('verbose') > 0 + with CaptureQueriesContext(connection) as context: + yield + num_queries = len(context) + failed = num != num_queries if exact else num < num_queries + if failed: + msg = "Expected to perform {} queries {}{}".format( + num, + '' if exact else 'or less ', + 'but {} were done'.format(num_queries) + ) + if verbose: + sqls = (q['sql'] for q in context.captured_queries) + msg += '\n\nQueries:\n========\n\n%s' % '\n\n'.join(sqls) + else: + msg += " (add -v option to show queries)" + pytest.fail(msg) + - @contextmanager - def _assert_num_queries(num): - with CaptureQueriesContext(connection) as context: - yield - if num != len(context): - msg = "Expected to perform %s queries but %s were done" % (num, len(context)) - if pytestconfig.getoption('verbose') > 0: - sqls = (q['sql'] for q in context.captured_queries) - msg += '\n\nQueries:\n========\n\n%s' % '\n\n'.join(sqls) - else: - msg += " (add -v option to show queries)" - pytest.fail(msg) - - return _assert_num_queries +@pytest.fixture(scope='function') +def django_assert_num_queries(pytestconfig): + return partial(_assert_num_queries, pytestconfig) + + +@pytest.fixture(scope='function') +def django_assert_max_num_queries(pytestconfig): + return partial(_assert_num_queries, pytestconfig, exact=False) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 20ed3ec77..03eb66dc6 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -16,6 +16,7 @@ from .django_compat import is_django_unittest # noqa from .fixtures import django_assert_num_queries # noqa +from .fixtures import django_assert_max_num_queries # noqa from .fixtures import django_db_setup # noqa from .fixtures import django_db_use_migrations # noqa from .fixtures import django_db_keepdb # noqa diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 26cb5bead..0efe95a88 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -64,8 +64,22 @@ def test_django_assert_num_queries_db(django_assert_num_queries): Item.objects.create(name='quux') +@pytest.mark.django_db +def test_django_assert_max_num_queries_db(django_assert_max_num_queries): + with django_assert_max_num_queries(2): + Item.objects.create(name='1-foo') + Item.objects.create(name='2-bar') + + with pytest.raises(pytest.fail.Exception): + with django_assert_max_num_queries(2): + Item.objects.create(name='1-foo') + Item.objects.create(name='2-bar') + Item.objects.create(name='3-quux') + + @pytest.mark.django_db(transaction=True) -def test_django_assert_num_queries_transactional_db(transactional_db, django_assert_num_queries): +def test_django_assert_num_queries_transactional_db( + transactional_db, django_assert_num_queries): with transaction.atomic(): with django_assert_num_queries(3): From b1d93fc5fb021560a5fe2aafd7fb200ab852057e Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 26 Jul 2018 06:15:18 +0200 Subject: [PATCH 2/3] _assert_num_queries: yield context --- docs/helpers.rst | 4 +++- pytest_django/fixtures.py | 6 ++++-- tests/test_fixtures.py | 18 ++++++++++++++---- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 1219f6746..4c936f17a 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -259,11 +259,13 @@ Example :: def test_queries(django_assert_num_queries): - with django_assert_num_queries(3): + with django_assert_num_queries(3) as captured: Item.objects.create('foo') Item.objects.create('bar') Item.objects.create('baz') + assert 'foo' in captured.captured_queries[0]['sql'] + ``django_assert_max_num_queries`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 823edbffb..f840edf4e 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -360,14 +360,16 @@ def _assert_num_queries(config, num, exact=True): from django.test.utils import CaptureQueriesContext verbose = config.getoption('verbose') > 0 with CaptureQueriesContext(connection) as context: - yield + yield context num_queries = len(context) failed = num != num_queries if exact else num < num_queries if failed: msg = "Expected to perform {} queries {}{}".format( num, '' if exact else 'or less ', - 'but {} were done'.format(num_queries) + 'but {} done'.format( + num_queries == 1 and '1 was' or '%d were' % (num_queries,) + ) ) if verbose: sqls = (q['sql'] for q in context.captured_queries) diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 0efe95a88..9f17d96cb 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -59,9 +59,13 @@ def test_django_assert_num_queries_db(django_assert_num_queries): Item.objects.create(name='bar') Item.objects.create(name='baz') - with pytest.raises(pytest.fail.Exception): - with django_assert_num_queries(2): + with pytest.raises(pytest.fail.Exception) as excinfo: + with django_assert_num_queries(2) as captured: Item.objects.create(name='quux') + assert excinfo.value.args == ( + 'Expected to perform 2 queries but 1 was done ' + '(add -v option to show queries)',) + assert len(captured.captured_queries) == 1 @pytest.mark.django_db @@ -70,12 +74,18 @@ def test_django_assert_max_num_queries_db(django_assert_max_num_queries): Item.objects.create(name='1-foo') Item.objects.create(name='2-bar') - with pytest.raises(pytest.fail.Exception): - with django_assert_max_num_queries(2): + with pytest.raises(pytest.fail.Exception) as excinfo: + with django_assert_max_num_queries(2) as captured: Item.objects.create(name='1-foo') Item.objects.create(name='2-bar') Item.objects.create(name='3-quux') + assert excinfo.value.args == ( + 'Expected to perform 2 queries or less but 3 were done ' + '(add -v option to show queries)',) + assert len(captured.captured_queries) == 3 + assert '1-foo' in captured.captured_queries[0]['sql'] + @pytest.mark.django_db(transaction=True) def test_django_assert_num_queries_transactional_db( From 7a655f87fe1f2c93c641c16f485644d38e706347 Mon Sep 17 00:00:00 2001 From: Daniel Hahler Date: Thu, 26 Jul 2018 06:38:48 +0200 Subject: [PATCH 3/3] _assert_num_queries: support any connection --- docs/helpers.rst | 12 ++++++++++-- pytest_django/fixtures.py | 7 +++++-- tests/test_fixtures.py | 15 +++++++++++++++ 3 files changed, 30 insertions(+), 4 deletions(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 4c936f17a..9f1c2f99f 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -249,8 +249,13 @@ Example ``django_assert_num_queries`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. fixture:: django_assert_num_queries + This fixture allows to check for an expected number of DB queries. -It currently only supports the default database. + +It wraps `django.test.utils.CaptureQueriesContext`. A non-default DB +connection can be passed in using the `connection` keyword argument, and it +will yield the wrapped CaptureQueriesContext instance. Example @@ -270,8 +275,11 @@ Example ``django_assert_max_num_queries`` ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +.. fixture:: django_assert_max_num_queries + This fixture allows to check for an expected maximum number of DB queries. -It currently only supports the default database. + +It is a specialized version of :fixture:`django_assert_num_queries`. Example diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index f840edf4e..9d6ac7054 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -355,9 +355,12 @@ def _live_server_helper(request): @contextmanager -def _assert_num_queries(config, num, exact=True): - from django.db import connection +def _assert_num_queries(config, num, exact=True, connection=None): from django.test.utils import CaptureQueriesContext + + if connection is None: + from django.db import connection + verbose = config.getoption('verbose') > 0 with CaptureQueriesContext(connection) as context: yield context diff --git a/tests/test_fixtures.py b/tests/test_fixtures.py index 9f17d96cb..891a64f8e 100644 --- a/tests/test_fixtures.py +++ b/tests/test_fixtures.py @@ -138,6 +138,21 @@ def test_queries(django_assert_num_queries): assert result.ret == 1 +@pytest.mark.django_db +def test_django_assert_num_queries_db_connection(django_assert_num_queries): + from django.db import connection + + with django_assert_num_queries(1, connection=connection): + Item.objects.create(name='foo') + + with django_assert_num_queries(1, connection=None): + Item.objects.create(name='foo') + + with pytest.raises(AttributeError): + with django_assert_num_queries(1, connection=False): + pass + + class TestSettings: """Tests for the settings fixture, order matters"""