From c56c818aad930cf21d47942e9792ce664f7861a6 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 15:37:46 -0400 Subject: [PATCH 01/33] Fixes issue with maxDiff and verbosity --- pytest_django/plugin.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 08d961a6..f5946fc0 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -374,6 +374,10 @@ def pytest_configure(config: pytest.Config) -> None: # it's fully initialized here. _setup_django(config) + if config.option.verbose > 0: + from .asserts import test_case + test_case.maxDiff = None + @pytest.hookimpl() def pytest_report_header(config: pytest.Config) -> list[str] | None: From c551601022360005a8045b003bc47b051c176d62 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 15:39:36 -0400 Subject: [PATCH 02/33] Update plugin.py --- pytest_django/plugin.py | 1 + 1 file changed, 1 insertion(+) diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index f5946fc0..65568f6b 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -376,6 +376,7 @@ def pytest_configure(config: pytest.Config) -> None: if config.option.verbose > 0: from .asserts import test_case + test_case.maxDiff = None From f4d6ea9123a8e8d8a6fad9b2df394c60c5af4567 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:02:56 -0400 Subject: [PATCH 03/33] Adds tests --- tests/test_asserts.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index c9a01ec7..e2397960 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -71,3 +71,29 @@ def test_sanity() -> None: pass assert assertContains.__doc__ + + +def test_assert_diff(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import pytest_django.asserts + + def test_assert(settings, mailoutbox): + pytest_django.asserts.assertEquals("a"* 10_000, "a") + """ + ) + result = django_pytester.runpytest_subprocess() + assert "[truncated]... != 'a'" in "\n".join([*results.stdout, *results.stderr]) + + +def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import pytest_django.asserts + + def test_assert(settings, mailoutbox): + pytest_django.asserts.assertEquals("a" * 10_000, "a") + """ + ) + result = django_pytester.runpytest_subprocess("-v") + assert "a" * 10_000 in "\n".join([*results.stdout, *results.stderr]) From 0a6ef42cd485a29ef7b69728d9843f0195bf3006 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:04:09 -0400 Subject: [PATCH 04/33] Minor adjustment --- tests/test_asserts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index e2397960..0c3fc128 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -84,6 +84,7 @@ def test_assert(settings, mailoutbox): ) result = django_pytester.runpytest_subprocess() assert "[truncated]... != 'a'" in "\n".join([*results.stdout, *results.stderr]) + result.assert_outcomes(errored=1) def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: @@ -97,3 +98,4 @@ def test_assert(settings, mailoutbox): ) result = django_pytester.runpytest_subprocess("-v") assert "a" * 10_000 in "\n".join([*results.stdout, *results.stderr]) + result.assert_outcomes(errored=1) From 8b26d9b8de04c857fb2e42583c3c288b872485c7 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:06:00 -0400 Subject: [PATCH 05/33] Better tests --- tests/test_asserts.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 0c3fc128..665ca17e 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -78,13 +78,16 @@ def test_assert_diff(django_pytester: DjangoPytester) -> None: """ import pytest_django.asserts - def test_assert(settings, mailoutbox): + def test_test_case(): + assert pytest_django.asserts.test_case.maxDiff is not None + + def test_assert(): pytest_django.asserts.assertEquals("a"* 10_000, "a") """ ) result = django_pytester.runpytest_subprocess() assert "[truncated]... != 'a'" in "\n".join([*results.stdout, *results.stderr]) - result.assert_outcomes(errored=1) + result.assert_outcomes(passed=1, errored=1) def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: @@ -92,10 +95,13 @@ def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: """ import pytest_django.asserts - def test_assert(settings, mailoutbox): + def test_test_case(): + assert pytest_django.asserts.test_case.maxDiff is None + + def test_assert(): pytest_django.asserts.assertEquals("a" * 10_000, "a") """ ) result = django_pytester.runpytest_subprocess("-v") assert "a" * 10_000 in "\n".join([*results.stdout, *results.stderr]) - result.assert_outcomes(errored=1) + result.assert_outcomes(passed=1, errored=1) From 3962bd585877999f8715f3c2e1e3e7abc5d2904f Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:10:55 -0400 Subject: [PATCH 06/33] Update test_asserts.py --- tests/test_asserts.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 665ca17e..4d825c27 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -11,6 +11,8 @@ import pytest_django from pytest_django.asserts import __all__ as asserts_all +from .helpers import DjangoPytester + def _get_actual_assertions_names() -> list[str]: """ @@ -82,7 +84,7 @@ def test_test_case(): assert pytest_django.asserts.test_case.maxDiff is not None def test_assert(): - pytest_django.asserts.assertEquals("a"* 10_000, "a") + pytest_django.asserts.assertXMLEqual("a" * 10_000, "a") """ ) result = django_pytester.runpytest_subprocess() @@ -99,7 +101,7 @@ def test_test_case(): assert pytest_django.asserts.test_case.maxDiff is None def test_assert(): - pytest_django.asserts.assertEquals("a" * 10_000, "a") + pytest_django.asserts.assertXMLEqual("a" * 10_000, "a") """ ) result = django_pytester.runpytest_subprocess("-v") From 3520685ec29ec1cf267e5d3bf0c4f58b131d515c Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:12:21 -0400 Subject: [PATCH 07/33] Update test_asserts.py --- tests/test_asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 4d825c27..dc47a042 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -8,11 +8,11 @@ import pytest +from .helpers import DjangoPytester + import pytest_django from pytest_django.asserts import __all__ as asserts_all -from .helpers import DjangoPytester - def _get_actual_assertions_names() -> list[str]: """ From 0c796b963a51240c15ede3fef38362a5d85a88e6 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:13:16 -0400 Subject: [PATCH 08/33] Update test_asserts.py --- tests/test_asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index dc47a042..01159485 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -88,7 +88,7 @@ def test_assert(): """ ) result = django_pytester.runpytest_subprocess() - assert "[truncated]... != 'a'" in "\n".join([*results.stdout, *results.stderr]) + assert "[truncated]... != 'a'" in "\n".join([*result.stdout, *result.stderr]) result.assert_outcomes(passed=1, errored=1) @@ -105,5 +105,5 @@ def test_assert(): """ ) result = django_pytester.runpytest_subprocess("-v") - assert "a" * 10_000 in "\n".join([*results.stdout, *results.stderr]) + assert "a" * 10_000 in "\n".join([*result.stdout, *result.stderr]) result.assert_outcomes(passed=1, errored=1) From c2d9f7268f0f6c143bd88cb7559a61bb96818f6b Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:15:19 -0400 Subject: [PATCH 09/33] Update test_asserts.py --- tests/test_asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 01159485..d507b312 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -89,7 +89,7 @@ def test_assert(): ) result = django_pytester.runpytest_subprocess() assert "[truncated]... != 'a'" in "\n".join([*result.stdout, *result.stderr]) - result.assert_outcomes(passed=1, errored=1) + result.assert_outcomes(passed=1, errors=1) def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: @@ -106,4 +106,4 @@ def test_assert(): ) result = django_pytester.runpytest_subprocess("-v") assert "a" * 10_000 in "\n".join([*result.stdout, *result.stderr]) - result.assert_outcomes(passed=1, errored=1) + result.assert_outcomes(passed=1, errors=1) From 14ce12a6afb65812033a46d5ca09580b6a1bb6fa Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:20:41 -0400 Subject: [PATCH 10/33] Update test_asserts.py --- tests/test_asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index d507b312..eb0b32d8 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -88,7 +88,7 @@ def test_assert(): """ ) result = django_pytester.runpytest_subprocess() - assert "[truncated]... != 'a'" in "\n".join([*result.stdout, *result.stderr]) + assert "[truncated]... != 'a'" in "\n".join([*result.outlines, *result.errlines]) result.assert_outcomes(passed=1, errors=1) @@ -105,5 +105,5 @@ def test_assert(): """ ) result = django_pytester.runpytest_subprocess("-v") - assert "a" * 10_000 in "\n".join([*result.stdout, *result.stderr]) + assert "a" * 10_000 in "\n".join([*result.outlines, *result.errlines]) result.assert_outcomes(passed=1, errors=1) From 567fa231d735943e17d4960599bc42f61d42eabc Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:24:19 -0400 Subject: [PATCH 11/33] Update test_asserts.py --- tests/test_asserts.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index eb0b32d8..08a9af60 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -75,6 +75,7 @@ def test_sanity() -> None: assert assertContains.__doc__ +@pytest.mark.django_project(create_manage_py=True) def test_assert_diff(django_pytester: DjangoPytester) -> None: django_pytester.create_test_module( """ @@ -92,6 +93,7 @@ def test_assert(): result.assert_outcomes(passed=1, errors=1) +@pytest.mark.django_project(create_manage_py=True) def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: django_pytester.create_test_module( """ From d42d733d45f716f08b7746e1a7ea3f1920bcf0dd Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Sun, 6 Apr 2025 16:27:03 -0400 Subject: [PATCH 12/33] Update test_asserts.py --- tests/test_asserts.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 08a9af60..1a73c792 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -90,7 +90,7 @@ def test_assert(): ) result = django_pytester.runpytest_subprocess() assert "[truncated]... != 'a'" in "\n".join([*result.outlines, *result.errlines]) - result.assert_outcomes(passed=1, errors=1) + result.assert_outcomes(passed=1, failed=1) @pytest.mark.django_project(create_manage_py=True) @@ -108,4 +108,4 @@ def test_assert(): ) result = django_pytester.runpytest_subprocess("-v") assert "a" * 10_000 in "\n".join([*result.outlines, *result.errlines]) - result.assert_outcomes(passed=1, errors=1) + result.assert_outcomes(passed=1, failed=1) From 4917b34588040c3649bdc173c4a536b8e6215ecb Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Mon, 7 Apr 2025 07:21:34 -0400 Subject: [PATCH 13/33] does this work?? --- pytest_django/fixtures.py | 112 ++++++++++++++++++++++---------------- pytest_django/plugin.py | 5 -- 2 files changed, 66 insertions(+), 51 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 6dc05fdb..a7db0a4d 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -216,15 +216,15 @@ def django_db_setup( @pytest.fixture() -def _django_db_helper( +def pytest_django_testcase_class( request: pytest.FixtureRequest, - django_db_setup: None, - django_db_blocker: DjangoDbBlocker, -) -> Generator[None, None, None]: +): if is_django_unittest(request): yield return + import django.test + marker = request.node.get_closest_marker("django_db") if marker: ( @@ -253,57 +253,72 @@ def _django_db_helper( "django_db_serialized_rollback" in request.fixturenames ) + if transactional: + test_case_class = django.test.TransactionTestCase + else: + test_case_class = django.test.TestCase + + _reset_sequences = reset_sequences + _serialized_rollback = serialized_rollback + _databases = databases + _available_apps = available_apps + + class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] + reset_sequences = _reset_sequences + serialized_rollback = _serialized_rollback + if _databases is not None: + databases = _databases + if _available_apps is not None: + available_apps = _available_apps + + # For non-transactional tests, skip executing `django.test.TestCase`'s + # `setUpClass`/`tearDownClass`, only execute the super class ones. + # + # `TestCase`'s class setup manages the `setUpTestData`/class-level + # transaction functionality. We don't use it; instead we (will) offer + # our own alternatives. So it only adds overhead, and does some things + # which conflict with our (planned) functionality, particularly, it + # closes all database connections in `tearDownClass` which inhibits + # wrapping tests in higher-scoped transactions. + # + # It's possible a new version of Django will add some unrelated + # functionality to these methods, in which case skipping them completely + # would not be desirable. Let's cross that bridge when we get there... + if not transactional: + + @classmethod + def setUpClass(cls) -> None: + super(django.test.TestCase, cls).setUpClass() + + @classmethod + def tearDownClass(cls) -> None: + super(django.test.TestCase, cls).tearDownClass() + + return PytestDjangoTestCase + + +@pytest.fixture() +def _django_db_helper( + request: pytest.FixtureRequest, + django_db_setup: None, + django_db_blocker: DjangoDbBlocker, + pytest_django_testcase_class, +) -> Generator[None, None, None]: + if is_django_unittest(request): + yield + return + with django_db_blocker.unblock(): import django.db - import django.test - if transactional: - test_case_class = django.test.TransactionTestCase - else: - test_case_class = django.test.TestCase - - _reset_sequences = reset_sequences - _serialized_rollback = serialized_rollback - _databases = databases - _available_apps = available_apps - - class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] - reset_sequences = _reset_sequences - serialized_rollback = _serialized_rollback - if _databases is not None: - databases = _databases - if _available_apps is not None: - available_apps = _available_apps - - # For non-transactional tests, skip executing `django.test.TestCase`'s - # `setUpClass`/`tearDownClass`, only execute the super class ones. - # - # `TestCase`'s class setup manages the `setUpTestData`/class-level - # transaction functionality. We don't use it; instead we (will) offer - # our own alternatives. So it only adds overhead, and does some things - # which conflict with our (planned) functionality, particularly, it - # closes all database connections in `tearDownClass` which inhibits - # wrapping tests in higher-scoped transactions. - # - # It's possible a new version of Django will add some unrelated - # functionality to these methods, in which case skipping them completely - # would not be desirable. Let's cross that bridge when we get there... - if not transactional: - - @classmethod - def setUpClass(cls) -> None: - super(django.test.TestCase, cls).setUpClass() - - @classmethod - def tearDownClass(cls) -> None: - super(django.test.TestCase, cls).tearDownClass() + PytestDjangoTestCase = pytest_django_testcase_class PytestDjangoTestCase.setUpClass() test_case = PytestDjangoTestCase(methodName="__init__") test_case._pre_setup() - yield + yield test_case test_case._post_teardown() @@ -379,6 +394,11 @@ def _set_suffix_to_test_databases(suffix: str) -> None: # ############### User visible fixtures ################ +@pytest.fixture() +def pytest_django_testcase(_django_db_helper: None) -> None: + yield _django_db_helper + + @pytest.fixture() def db(_django_db_helper: None) -> None: """Require a django test database. diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 1df87056..e8e629f4 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -374,11 +374,6 @@ def pytest_configure(config: pytest.Config) -> None: # it's fully initialized here. _setup_django(config) - if config.option.verbose > 0: - from .asserts import test_case - - test_case.maxDiff = None - @pytest.hookimpl() def pytest_report_header(config: pytest.Config) -> list[str] | None: From 90d40b20f4bda70a1c24ed41bb65e52fd4dd1a1d Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 07:40:17 -0400 Subject: [PATCH 14/33] minor progress --- pytest_django/fixtures.py | 14 +++++++------- pytest_django/plugin.py | 2 ++ 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index a7db0a4d..b8662693 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -60,6 +60,8 @@ "django_username_field", "live_server", "rf", + "django_testcase", + "django_testcase_class", "settings", "transactional_db", ] @@ -216,7 +218,7 @@ def django_db_setup( @pytest.fixture() -def pytest_django_testcase_class( +def django_testcase_class( request: pytest.FixtureRequest, ): if is_django_unittest(request): @@ -294,7 +296,7 @@ def setUpClass(cls) -> None: def tearDownClass(cls) -> None: super(django.test.TestCase, cls).tearDownClass() - return PytestDjangoTestCase + yield PytestDjangoTestCase @pytest.fixture() @@ -302,16 +304,14 @@ def _django_db_helper( request: pytest.FixtureRequest, django_db_setup: None, django_db_blocker: DjangoDbBlocker, - pytest_django_testcase_class, + django_testcase_class, ) -> Generator[None, None, None]: if is_django_unittest(request): yield return with django_db_blocker.unblock(): - import django.db - - PytestDjangoTestCase = pytest_django_testcase_class + PytestDjangoTestCase = django_testcase_class PytestDjangoTestCase.setUpClass() @@ -395,7 +395,7 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture() -def pytest_django_testcase(_django_db_helper: None) -> None: +def django_testcase(_django_db_helper: None) -> None: yield _django_db_helper diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index e8e629f4..0e7cd83d 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -44,6 +44,8 @@ django_username_field, # noqa: F401 live_server, # noqa: F401 rf, # noqa: F401 + django_testcase, # noqa: F401 + django_testcase_class, # noqa: F401 settings, # noqa: F401 transactional_db, # noqa: F401 validate_django_db, From 636771b5af3681ba97bdf4f4ece4f6d10510c751 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:03:45 -0400 Subject: [PATCH 15/33] . --- pytest_django/fixtures.py | 21 ++++++++++--------- tests/test_asserts.py | 44 +++++++++------------------------------ 2 files changed, 21 insertions(+), 44 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index b8662693..40835099 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -21,7 +21,7 @@ Tuple, Union, ) - + import pytest from . import live_server_helper @@ -220,7 +220,7 @@ def django_db_setup( @pytest.fixture() def django_testcase_class( request: pytest.FixtureRequest, -): +) -> Generator[None, None, type[django.test.TestCase | django.test.TransactionTestCase]]: if is_django_unittest(request): yield return @@ -264,6 +264,7 @@ def django_testcase_class( _serialized_rollback = serialized_rollback _databases = databases _available_apps = available_apps + _verbose = request.config.option.verbose > 0 class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] reset_sequences = _reset_sequences @@ -272,6 +273,8 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] databases = _databases if _available_apps is not None: available_apps = _available_apps + if _verbose: + maxDiff = None # For non-transactional tests, skip executing `django.test.TestCase`'s # `setUpClass`/`tearDownClass`, only execute the super class ones. @@ -304,27 +307,25 @@ def _django_db_helper( request: pytest.FixtureRequest, django_db_setup: None, django_db_blocker: DjangoDbBlocker, - django_testcase_class, + django_testcase_class: type[django.test.TestCase | django.test.TransactionTestCase], ) -> Generator[None, None, None]: if is_django_unittest(request): yield return with django_db_blocker.unblock(): - PytestDjangoTestCase = django_testcase_class - - PytestDjangoTestCase.setUpClass() + django_testcase_class.setUpClass() - test_case = PytestDjangoTestCase(methodName="__init__") + test_case = django_testcase_class(methodName="__init__") test_case._pre_setup() yield test_case test_case._post_teardown() - PytestDjangoTestCase.tearDownClass() + django_testcase_class.tearDownClass() - PytestDjangoTestCase.doClassCleanups() + django_testcase_class.doClassCleanups() def _django_db_signature( @@ -395,7 +396,7 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture() -def django_testcase(_django_db_helper: None) -> None: +def django_testcase(_django_db_helper: None) -> Generator[None, None, django.test.TestCase | django.test.TransactionTestCase | None]: yield _django_db_helper diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 1a73c792..21f043f9 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -13,6 +13,11 @@ import pytest_django from pytest_django.asserts import __all__ as asserts_all +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import django.test + def _get_actual_assertions_names() -> list[str]: """ @@ -75,37 +80,8 @@ def test_sanity() -> None: assert assertContains.__doc__ -@pytest.mark.django_project(create_manage_py=True) -def test_assert_diff(django_pytester: DjangoPytester) -> None: - django_pytester.create_test_module( - """ - import pytest_django.asserts - - def test_test_case(): - assert pytest_django.asserts.test_case.maxDiff is not None - - def test_assert(): - pytest_django.asserts.assertXMLEqual("a" * 10_000, "a") - """ - ) - result = django_pytester.runpytest_subprocess() - assert "[truncated]... != 'a'" in "\n".join([*result.outlines, *result.errlines]) - result.assert_outcomes(passed=1, failed=1) - - -@pytest.mark.django_project(create_manage_py=True) -def test_assert_diff_verbose(django_pytester: DjangoPytester) -> None: - django_pytester.create_test_module( - """ - import pytest_django.asserts - - def test_test_case(): - assert pytest_django.asserts.test_case.maxDiff is None - - def test_assert(): - pytest_django.asserts.assertXMLEqual("a" * 10_000, "a") - """ - ) - result = django_pytester.runpytest_subprocess("-v") - assert "a" * 10_000 in "\n".join([*result.outlines, *result.errlines]) - result.assert_outcomes(passed=1, failed=1) +def test_real_assert(django_testcase: django.test.TestCase) -> None: + django_testcase.assertEqual("a", "a") + + with pytest.raises(AssertionError): + django_testcase.assertXMLEqual("a" * 10_000, "a") From 60578dc4800b6518eb3718a21cd519e81e9d6f89 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:09:01 -0400 Subject: [PATCH 16/33] . --- pytest_django/fixtures.py | 15 ++++++++------- pytest_django/plugin.py | 4 ++-- tests/test_asserts.py | 6 ++---- 3 files changed, 12 insertions(+), 13 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 40835099..68065635 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -21,7 +21,7 @@ Tuple, Union, ) - + import pytest from . import live_server_helper @@ -56,12 +56,12 @@ "django_db_reset_sequences", "django_db_serialized_rollback", "django_db_setup", + "django_testcase", + "django_testcase_class", "django_user_model", "django_username_field", "live_server", "rf", - "django_testcase", - "django_testcase_class", "settings", "transactional_db", ] @@ -220,7 +220,7 @@ def django_db_setup( @pytest.fixture() def django_testcase_class( request: pytest.FixtureRequest, -) -> Generator[None, None, type[django.test.TestCase | django.test.TransactionTestCase]]: +) -> Generator[None, None, type[django.test.TestCase]]: if is_django_unittest(request): yield return @@ -300,6 +300,7 @@ def tearDownClass(cls) -> None: super(django.test.TestCase, cls).tearDownClass() yield PytestDjangoTestCase + return @pytest.fixture() @@ -307,7 +308,7 @@ def _django_db_helper( request: pytest.FixtureRequest, django_db_setup: None, django_db_blocker: DjangoDbBlocker, - django_testcase_class: type[django.test.TestCase | django.test.TransactionTestCase], + django_testcase_class: type[django.test.TestCase], ) -> Generator[None, None, None]: if is_django_unittest(request): yield @@ -396,8 +397,8 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture() -def django_testcase(_django_db_helper: None) -> Generator[None, None, django.test.TestCase | django.test.TransactionTestCase | None]: - yield _django_db_helper +def django_testcase(_django_db_helper: None) -> None: + return _django_db_helper @pytest.fixture() diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index 0e7cd83d..c946c525 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -40,12 +40,12 @@ django_db_serialized_rollback, # noqa: F401 django_db_setup, # noqa: F401 django_db_use_migrations, # noqa: F401 + django_testcase, # noqa: F401 + django_testcase_class, # noqa: F401 django_user_model, # noqa: F401 django_username_field, # noqa: F401 live_server, # noqa: F401 rf, # noqa: F401 - django_testcase, # noqa: F401 - django_testcase_class, # noqa: F401 settings, # noqa: F401 transactional_db, # noqa: F401 validate_django_db, diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 21f043f9..25417f98 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -5,15 +5,13 @@ from __future__ import annotations import inspect +from typing import TYPE_CHECKING import pytest -from .helpers import DjangoPytester - import pytest_django from pytest_django.asserts import __all__ as asserts_all -from typing import TYPE_CHECKING if TYPE_CHECKING: import django.test @@ -81,7 +79,7 @@ def test_sanity() -> None: def test_real_assert(django_testcase: django.test.TestCase) -> None: - django_testcase.assertEqual("a", "a") + django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): django_testcase.assertXMLEqual("a" * 10_000, "a") From 8ee49d0295dfa87f21b71feb59315107a9c757ed Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:21:26 -0400 Subject: [PATCH 17/33] Fixes test --- pytest_django/asserts.py | 10 ++++++++++ pytest_django/fixtures.py | 7 +++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pytest_django/asserts.py b/pytest_django/asserts.py index 14741066..ad456d20 100644 --- a/pytest_django/asserts.py +++ b/pytest_django/asserts.py @@ -4,6 +4,7 @@ from __future__ import annotations +import warnings from functools import wraps from typing import TYPE_CHECKING, Any, Callable, Sequence @@ -30,6 +31,15 @@ def _wrapper(name: str): @wraps(func) def assertion_func(*args, **kwargs): + message = ( + f"Using pytest_django.asserts.{name} is deprecated. " + f'Use fixture "django_testcase" and django_testcase.{name} instead.' + ) + warnings.warn( + message, + DeprecationWarning, + stacklevel=2, + ) return func(*args, **kwargs) return assertion_func diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 68065635..8d90ae10 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -220,9 +220,9 @@ def django_db_setup( @pytest.fixture() def django_testcase_class( request: pytest.FixtureRequest, -) -> Generator[None, None, type[django.test.TestCase]]: +) -> Generator[type[django.test.TestCase] | None, None, None]: if is_django_unittest(request): - yield + yield None return import django.test @@ -300,7 +300,6 @@ def tearDownClass(cls) -> None: super(django.test.TestCase, cls).tearDownClass() yield PytestDjangoTestCase - return @pytest.fixture() @@ -397,7 +396,7 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture() -def django_testcase(_django_db_helper: None) -> None: +def django_testcase(_django_db_helper: None) -> django.test.TestCase | None: return _django_db_helper From 5da273a2b2ddb4e448bbd011117a7a2588467374 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:22:16 -0400 Subject: [PATCH 18/33] Removes maxDiff for now --- pytest_django/fixtures.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 8d90ae10..597d20dd 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -264,7 +264,6 @@ def django_testcase_class( _serialized_rollback = serialized_rollback _databases = databases _available_apps = available_apps - _verbose = request.config.option.verbose > 0 class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] reset_sequences = _reset_sequences @@ -273,8 +272,6 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] databases = _databases if _available_apps is not None: available_apps = _available_apps - if _verbose: - maxDiff = None # For non-transactional tests, skip executing `django.test.TestCase`'s # `setUpClass`/`tearDownClass`, only execute the super class ones. From ccbead49cfba4028d0c1a1f9e61627eb0cde388c Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:29:01 -0400 Subject: [PATCH 19/33] Minor updates --- pytest_django/fixtures.py | 3 +-- pytest_django/plugin.py | 1 - 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 597d20dd..98072815 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -57,7 +57,6 @@ "django_db_serialized_rollback", "django_db_setup", "django_testcase", - "django_testcase_class", "django_user_model", "django_username_field", "live_server", @@ -393,7 +392,7 @@ def _set_suffix_to_test_databases(suffix: str) -> None: @pytest.fixture() -def django_testcase(_django_db_helper: None) -> django.test.TestCase | None: +def django_testcase(_django_db_helper: django.test.TestCase | None) -> django.test.TestCase | None: return _django_db_helper diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index c946c525..cee9d989 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -41,7 +41,6 @@ django_db_setup, # noqa: F401 django_db_use_migrations, # noqa: F401 django_testcase, # noqa: F401 - django_testcase_class, # noqa: F401 django_user_model, # noqa: F401 django_username_field, # noqa: F401 live_server, # noqa: F401 From a91d2cebdd55a8afd288881efc31cb82317f29f8 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:44:06 -0400 Subject: [PATCH 20/33] Fixes tests --- pytest_django/fixtures.py | 1 + pytest_django/plugin.py | 1 + tests/test_asserts.py | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 38 insertions(+) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 98072815..496ff385 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -57,6 +57,7 @@ "django_db_serialized_rollback", "django_db_setup", "django_testcase", + "django_testcase_class", "django_user_model", "django_username_field", "live_server", diff --git a/pytest_django/plugin.py b/pytest_django/plugin.py index cee9d989..c946c525 100644 --- a/pytest_django/plugin.py +++ b/pytest_django/plugin.py @@ -41,6 +41,7 @@ django_db_setup, # noqa: F401 django_db_use_migrations, # noqa: F401 django_testcase, # noqa: F401 + django_testcase_class, # noqa: F401 django_user_model, # noqa: F401 django_username_field, # noqa: F401 live_server, # noqa: F401 diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 25417f98..03a5fa96 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -7,8 +7,11 @@ import inspect from typing import TYPE_CHECKING +import django.test import pytest +from .helpers import DjangoPytester + import pytest_django from pytest_django.asserts import __all__ as asserts_all @@ -83,3 +86,36 @@ def test_real_assert(django_testcase: django.test.TestCase) -> None: with pytest.raises(AssertionError): django_testcase.assertXMLEqual("a" * 10_000, "a") + + +class TestDjangoAssert(django.test.TestCase): + def test_real_assert(django_testcase: django.test.TestCase) -> None: + django_testcase.assertEqual("a", "a") # noqa: PT009 + + with pytest.raises(AssertionError): + django_testcase.assertXMLEqual("a" * 10_000, "a") + + +class TestInternalDjangoAssert: + def test_real_assert(self, django_testcase: django.test.TestCase) -> None: + django_testcase.assertEqual("a", "a") # noqa: PT009 + assert not hasattr(self, "assertEqual") + + with pytest.raises(AssertionError): + django_testcase.assertXMLEqual("a" * 10_000, "a") + + +@pytest.mark.django_project(create_manage_py=True) +def test_unittest_assert(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import unittest + + class TestUnittestAssert(unittest.TestCase): + def test_real_assert(self, django_testcase: unittest.TestCase) -> None: + assert False + """ + ) + result = django_pytester.runpytest_subprocess() + result.assert_outcomes(failed=1) + assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str() From 11307216bf4016227b61098eae8bc0437ac16ce8 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 09:49:49 -0400 Subject: [PATCH 21/33] Minor rename --- tests/test_asserts.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 03a5fa96..79951ca9 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -5,7 +5,6 @@ from __future__ import annotations import inspect -from typing import TYPE_CHECKING import django.test import pytest @@ -16,10 +15,6 @@ from pytest_django.asserts import __all__ as asserts_all -if TYPE_CHECKING: - import django.test - - def _get_actual_assertions_names() -> list[str]: """ Returns list with names of all assertion helpers in Django. @@ -81,7 +76,7 @@ def test_sanity() -> None: assert assertContains.__doc__ -def test_real_assert(django_testcase: django.test.TestCase) -> None: +def test_fixture_assert(django_testcase: django.test.TestCase) -> None: django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): @@ -89,7 +84,7 @@ def test_real_assert(django_testcase: django.test.TestCase) -> None: class TestDjangoAssert(django.test.TestCase): - def test_real_assert(django_testcase: django.test.TestCase) -> None: + def test_fixture_assert(django_testcase: django.test.TestCase) -> None: django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): @@ -97,7 +92,7 @@ def test_real_assert(django_testcase: django.test.TestCase) -> None: class TestInternalDjangoAssert: - def test_real_assert(self, django_testcase: django.test.TestCase) -> None: + def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: django_testcase.assertEqual("a", "a") # noqa: PT009 assert not hasattr(self, "assertEqual") @@ -112,10 +107,13 @@ def test_unittest_assert(django_pytester: DjangoPytester) -> None: import unittest class TestUnittestAssert(unittest.TestCase): - def test_real_assert(self, django_testcase: unittest.TestCase) -> None: + def test_fixture_assert(self, django_testcase: unittest.TestCase) -> None: assert False + + def test_normal_assert(self) -> None: + self.assertEqual("a", "a") """ ) result = django_pytester.runpytest_subprocess() - result.assert_outcomes(failed=1) + result.assert_outcomes(failed=1, passed=1) assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str() From 91c867b16316abec49f042109a050bc8a702aa00 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 10:07:13 -0400 Subject: [PATCH 22/33] Adds docs --- docs/database.rst | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/docs/database.rst b/docs/database.rst index c4410a6a..37b47d36 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -267,6 +267,22 @@ tests. This fixture is by default requested from :fixture:`django_db_setup`. + +django_testcase +""""""""""""""" + +.. fixture:: django_testcase + +Provides access to Django's test case instance. + +:fixture:`django_testcase` can be used to access Django's `custom assertion methods `_ that are useful for testing web applications:: + + def test_add(django_testcase): + django_testcase.assertEqual(1 + 1, 2) + django_testcase.assertXMLEqual(..., ...) + django_testcase.assertJSONEqual(..., ...) + + django_db_blocker """"""""""""""""" From c8fccc0d2d0756bbdec6eb689c9a1759822894d9 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 10:12:51 -0400 Subject: [PATCH 23/33] Minor --- docs/database.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index 37b47d36..5c0b43e6 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -273,9 +273,10 @@ django_testcase .. fixture:: django_testcase -Provides access to Django's test case instance. +The ``django_testcase`` fixture provides access to Django's custom assertion +methods in your pytest-style tests. -:fixture:`django_testcase` can be used to access Django's `custom assertion methods `_ that are useful for testing web applications:: +Example usage:: def test_add(django_testcase): django_testcase.assertEqual(1 + 1, 2) From b75db55d5f2098404c0e3b4a6cef79b578366394 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 10:32:07 -0400 Subject: [PATCH 24/33] . --- docs/database.rst | 17 ---- docs/helpers.rst | 226 +++++----------------------------------------- 2 files changed, 23 insertions(+), 220 deletions(-) diff --git a/docs/database.rst b/docs/database.rst index 5c0b43e6..c4410a6a 100644 --- a/docs/database.rst +++ b/docs/database.rst @@ -267,23 +267,6 @@ tests. This fixture is by default requested from :fixture:`django_db_setup`. - -django_testcase -""""""""""""""" - -.. fixture:: django_testcase - -The ``django_testcase`` fixture provides access to Django's custom assertion -methods in your pytest-style tests. - -Example usage:: - - def test_add(django_testcase): - django_testcase.assertEqual(1 + 1, 2) - django_testcase.assertXMLEqual(..., ...) - django_testcase.assertJSONEqual(..., ...) - - django_db_blocker """"""""""""""""" diff --git a/docs/helpers.rst b/docs/helpers.rst index c9e189dd..7e4f51a1 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -7,11 +7,7 @@ Assertions ---------- All of Django's :class:`~django:django.test.TestCase` -:ref:`django:assertions` are available in ``pytest_django.asserts``, e.g. - -:: - - from pytest_django.asserts import assertTemplateUsed +:ref:`django:assertions` are available in via the :fixture:`django_testcase` fixture. Markers ------- @@ -284,6 +280,27 @@ Example Using the `admin_client` fixture will cause the test to automatically be marked for database use (no need to specify the :func:`~pytest.mark.django_db` mark). +.. fixture:: django_testcase + +``django_testcase`` - Django test case assertions +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +Instance of the test case class. This fixture is particularly useful when you want +to use Django's specialized assertions for testing web applications. + +Example +""""""" + +:: + + def test_add(django_testcase): + django_testcase.assertEqual(1 + 1, 2) + django_testcase.assertXMLEqual(..., ...) + django_testcase.assertJSONEqual(..., ...) + + with django_testcase.assertNumQueries(2): + some_function() + .. fixture:: admin_user ``admin_user`` - an admin user (superuser) @@ -395,201 +412,4 @@ created in data migrations, you should add the * ``db`` * ``transactional_db`` - In addition, using ``live_server`` or ``django_db_reset_sequences`` will also - trigger transactional database access, and ``django_db_serialized_rollback`` - regular database access, if not specified. - -.. fixture:: settings - -``settings`` -~~~~~~~~~~~~ - -This fixture will provide a handle on the Django settings module, and -automatically revert any changes made to the settings (modifications, additions -and deletions). - -Example -""""""" - -:: - - def test_with_specific_settings(settings): - settings.USE_TZ = True - assert settings.USE_TZ - - -.. fixture:: django_assert_num_queries - -``django_assert_num_queries`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. py:function:: django_assert_num_queries(num, connection=None, info=None, *, using=None) - - :param num: expected number of queries - :param connection: optional database connection - :param str info: optional info message to display on failure - :param str using: optional database alias - -This fixture allows to check for an expected number of DB queries. - -If the assertion failed, the executed queries can be shown by using -the verbose command line option. - -It wraps ``django.test.utils.CaptureQueriesContext`` and yields the wrapped -``CaptureQueriesContext`` instance. - -Example usage:: - - def test_queries(django_assert_num_queries): - 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'] - -If you use type annotations, you can annotate the fixture like this:: - - from pytest_django import DjangoAssertNumQueries - - def test_num_queries( - django_assert_num_queries: DjangoAssertNumQueries, - ): - ... - - -.. fixture:: django_assert_max_num_queries - -``django_assert_max_num_queries`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. py:function:: django_assert_max_num_queries(num, connection=None, info=None, *, using=None) - - :param num: expected maximum number of queries - :param connection: optional database connection - :param str info: optional info message to display on failure - :param str using: optional database alias - -This fixture allows to check for an expected maximum number of DB queries. - -It is a specialized version of :fixture:`django_assert_num_queries`. - -Example usage:: - - def test_max_queries(django_assert_max_num_queries): - with django_assert_max_num_queries(2): - Item.objects.create('foo') - Item.objects.create('bar') - -If you use type annotations, you can annotate the fixture like this:: - - from pytest_django import DjangoAssertNumQueries - - def test_max_num_queries( - django_assert_max_num_queries: DjangoAssertNumQueries, - ): - ... - - -.. fixture:: django_capture_on_commit_callbacks - -``django_capture_on_commit_callbacks`` -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - -.. py:function:: django_capture_on_commit_callbacks(*, using=DEFAULT_DB_ALIAS, execute=False) - - :param using: - The alias of the database connection to capture callbacks for. - :param execute: - If True, all the callbacks will be called as the context manager exits, if - no exception occurred. This emulates a commit after the wrapped block of - code. - -.. versionadded:: 4.4 - -Returns a context manager that captures -:func:`transaction.on_commit() ` callbacks for -the given database connection. It returns a list that contains, on exit of the -context, the captured callback functions. From this list you can make assertions -on the callbacks or call them to invoke their side effects, emulating a commit. - -Avoid this fixture in tests using ``transaction=True``; you are not likely to -get useful results. - -This fixture is based on Django's :meth:`django.test.TestCase.captureOnCommitCallbacks` -helper. - -Example usage:: - - def test_on_commit(client, mailoutbox, django_capture_on_commit_callbacks): - with django_capture_on_commit_callbacks(execute=True) as callbacks: - response = client.post( - '/contact/', - {'message': 'I like your site'}, - ) - - assert response.status_code == 200 - assert len(callbacks) == 1 - assert len(mailoutbox) == 1 - assert mailoutbox[0].subject == 'Contact Form' - assert mailoutbox[0].body == 'I like your site' - -If you use type annotations, you can annotate the fixture like this:: - - from pytest_django import DjangoCaptureOnCommitCallbacks - - def test_on_commit( - django_capture_on_commit_callbacks: DjangoCaptureOnCommitCallbacks, - ): - ... - -.. fixture:: mailoutbox - -``mailoutbox`` -~~~~~~~~~~~~~~ - -A clean email outbox to which Django-generated emails are sent. - -Example -""""""" - -:: - - from django.core import mail - - def test_mail(mailoutbox): - mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com']) - assert len(mailoutbox) == 1 - m = mailoutbox[0] - assert m.subject == 'subject' - assert m.body == 'body' - assert m.from_email == 'from@example.com' - assert list(m.to) == ['to@example.com'] - - -This uses the ``django_mail_patch_dns`` fixture, which patches -``DNS_NAME`` used by :mod:`django.core.mail` with the value from -the ``django_mail_dnsname`` fixture, which defaults to -"fake-tests.example.com". - - -Automatic cleanup ------------------ - -pytest-django provides some functionality to assure a clean and consistent environment -during tests. - -Clearing of site cache -~~~~~~~~~~~~~~~~~~~~~~ - -If ``django.contrib.sites`` is in your INSTALLED_APPS, Site cache will -be cleared for each test to avoid hitting the cache and causing the wrong Site -object to be returned by ``Site.objects.get_current()``. - - -Clearing of mail.outbox -~~~~~~~~~~~~~~~~~~~~~~~ - -``mail.outbox`` will be cleared for each pytest, to give each new test an empty -mailbox to work with. However, it's more "pytestic" to use the ``mailoutbox`` fixture described above -than to access ``mail.outbox``. + In addition, using ``live_server`` or ``django_db_reset_sequences`` \ No newline at end of file From 3b2e45fb8e395afa37d17c2714f69050237b197c Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Mon, 7 Apr 2025 10:34:31 -0400 Subject: [PATCH 25/33] oops --- docs/helpers.rst | 199 ++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 198 insertions(+), 1 deletion(-) diff --git a/docs/helpers.rst b/docs/helpers.rst index 7e4f51a1..69a17761 100644 --- a/docs/helpers.rst +++ b/docs/helpers.rst @@ -412,4 +412,201 @@ created in data migrations, you should add the * ``db`` * ``transactional_db`` - In addition, using ``live_server`` or ``django_db_reset_sequences`` \ No newline at end of file + In addition, using ``live_server`` or ``django_db_reset_sequences`` will also + trigger transactional database access, and ``django_db_serialized_rollback`` + regular database access, if not specified. + +.. fixture:: settings + +``settings`` +~~~~~~~~~~~~ + +This fixture will provide a handle on the Django settings module, and +automatically revert any changes made to the settings (modifications, additions +and deletions). + +Example +""""""" + +:: + + def test_with_specific_settings(settings): + settings.USE_TZ = True + assert settings.USE_TZ + + +.. fixture:: django_assert_num_queries + +``django_assert_num_queries`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. py:function:: django_assert_num_queries(num, connection=None, info=None, *, using=None) + + :param num: expected number of queries + :param connection: optional database connection + :param str info: optional info message to display on failure + :param str using: optional database alias + +This fixture allows to check for an expected number of DB queries. + +If the assertion failed, the executed queries can be shown by using +the verbose command line option. + +It wraps ``django.test.utils.CaptureQueriesContext`` and yields the wrapped +``CaptureQueriesContext`` instance. + +Example usage:: + + def test_queries(django_assert_num_queries): + 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'] + +If you use type annotations, you can annotate the fixture like this:: + + from pytest_django import DjangoAssertNumQueries + + def test_num_queries( + django_assert_num_queries: DjangoAssertNumQueries, + ): + ... + + +.. fixture:: django_assert_max_num_queries + +``django_assert_max_num_queries`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. py:function:: django_assert_max_num_queries(num, connection=None, info=None, *, using=None) + + :param num: expected maximum number of queries + :param connection: optional database connection + :param str info: optional info message to display on failure + :param str using: optional database alias + +This fixture allows to check for an expected maximum number of DB queries. + +It is a specialized version of :fixture:`django_assert_num_queries`. + +Example usage:: + + def test_max_queries(django_assert_max_num_queries): + with django_assert_max_num_queries(2): + Item.objects.create('foo') + Item.objects.create('bar') + +If you use type annotations, you can annotate the fixture like this:: + + from pytest_django import DjangoAssertNumQueries + + def test_max_num_queries( + django_assert_max_num_queries: DjangoAssertNumQueries, + ): + ... + + +.. fixture:: django_capture_on_commit_callbacks + +``django_capture_on_commit_callbacks`` +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +.. py:function:: django_capture_on_commit_callbacks(*, using=DEFAULT_DB_ALIAS, execute=False) + + :param using: + The alias of the database connection to capture callbacks for. + :param execute: + If True, all the callbacks will be called as the context manager exits, if + no exception occurred. This emulates a commit after the wrapped block of + code. + +.. versionadded:: 4.4 + +Returns a context manager that captures +:func:`transaction.on_commit() ` callbacks for +the given database connection. It returns a list that contains, on exit of the +context, the captured callback functions. From this list you can make assertions +on the callbacks or call them to invoke their side effects, emulating a commit. + +Avoid this fixture in tests using ``transaction=True``; you are not likely to +get useful results. + +This fixture is based on Django's :meth:`django.test.TestCase.captureOnCommitCallbacks` +helper. + +Example usage:: + + def test_on_commit(client, mailoutbox, django_capture_on_commit_callbacks): + with django_capture_on_commit_callbacks(execute=True) as callbacks: + response = client.post( + '/contact/', + {'message': 'I like your site'}, + ) + + assert response.status_code == 200 + assert len(callbacks) == 1 + assert len(mailoutbox) == 1 + assert mailoutbox[0].subject == 'Contact Form' + assert mailoutbox[0].body == 'I like your site' + +If you use type annotations, you can annotate the fixture like this:: + + from pytest_django import DjangoCaptureOnCommitCallbacks + + def test_on_commit( + django_capture_on_commit_callbacks: DjangoCaptureOnCommitCallbacks, + ): + ... + +.. fixture:: mailoutbox + +``mailoutbox`` +~~~~~~~~~~~~~~ + +A clean email outbox to which Django-generated emails are sent. + +Example +""""""" + +:: + + from django.core import mail + + def test_mail(mailoutbox): + mail.send_mail('subject', 'body', 'from@example.com', ['to@example.com']) + assert len(mailoutbox) == 1 + m = mailoutbox[0] + assert m.subject == 'subject' + assert m.body == 'body' + assert m.from_email == 'from@example.com' + assert list(m.to) == ['to@example.com'] + + +This uses the ``django_mail_patch_dns`` fixture, which patches +``DNS_NAME`` used by :mod:`django.core.mail` with the value from +the ``django_mail_dnsname`` fixture, which defaults to +"fake-tests.example.com". + + +Automatic cleanup +----------------- + +pytest-django provides some functionality to assure a clean and consistent environment +during tests. + +Clearing of site cache +~~~~~~~~~~~~~~~~~~~~~~ + +If ``django.contrib.sites`` is in your INSTALLED_APPS, Site cache will +be cleared for each test to avoid hitting the cache and causing the wrong Site +object to be returned by ``Site.objects.get_current()``. + + +Clearing of mail.outbox +~~~~~~~~~~~~~~~~~~~~~~~ + +``mail.outbox`` will be cleared for each pytest, to give each new test an empty +mailbox to work with. However, it's more "pytestic" to use the ``mailoutbox`` fixture described above +than to access ``mail.outbox``. From 26e6ebc9a6f1de1cfa6ed21ed4532242afeeda7e Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 08:36:48 -0400 Subject: [PATCH 26/33] Update test_asserts.py Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- tests/test_asserts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 79951ca9..f3559088 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -84,7 +84,7 @@ def test_fixture_assert(django_testcase: django.test.TestCase) -> None: class TestDjangoAssert(django.test.TestCase): - def test_fixture_assert(django_testcase: django.test.TestCase) -> None: + def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): From 48b032011f7382b194f246f00a77a62920d85dc3 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 08:39:29 -0400 Subject: [PATCH 27/33] Update test_asserts.py --- tests/test_asserts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index f3559088..c22fa5b1 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -85,6 +85,7 @@ def test_fixture_assert(django_testcase: django.test.TestCase) -> None: class TestDjangoAssert(django.test.TestCase): def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: + assert django_test == self django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): From 8f36bd8f6d7066bd7ee63881dc060cdc8162e668 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 09:10:23 -0400 Subject: [PATCH 28/33] Update test_asserts.py --- tests/test_asserts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index c22fa5b1..5058a144 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -85,7 +85,7 @@ def test_fixture_assert(django_testcase: django.test.TestCase) -> None: class TestDjangoAssert(django.test.TestCase): def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: - assert django_test == self + assert django_testcase == self django_testcase.assertEqual("a", "a") # noqa: PT009 with pytest.raises(AssertionError): From 8f086dc8a7941990e8bcc9c8b9c515b4f6a38a07 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 09:11:23 -0400 Subject: [PATCH 29/33] Update test_asserts.py --- tests/test_asserts.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 5058a144..5ef39711 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -94,6 +94,7 @@ def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: class TestInternalDjangoAssert: def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: + assert django_test != self django_testcase.assertEqual("a", "a") # noqa: PT009 assert not hasattr(self, "assertEqual") From 4efe1a4f3bb6d643535a042e47926d84af066fc8 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 09:39:38 -0400 Subject: [PATCH 30/33] Update test_asserts.py --- tests/test_asserts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 5ef39711..38b61f88 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -94,7 +94,7 @@ def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: class TestInternalDjangoAssert: def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: - assert django_test != self + assert django_testcase != self django_testcase.assertEqual("a", "a") # noqa: PT009 assert not hasattr(self, "assertEqual") From 48a658ef2a8bc7e2550114fd031bc4864189b0e4 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 09:46:44 -0400 Subject: [PATCH 31/33] Update test_asserts.py --- tests/test_asserts.py | 32 ++++++++++++++++++++++---------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 38b61f88..1cde54ba 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -83,15 +83,6 @@ def test_fixture_assert(django_testcase: django.test.TestCase) -> None: django_testcase.assertXMLEqual("a" * 10_000, "a") -class TestDjangoAssert(django.test.TestCase): - def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: - assert django_testcase == self - django_testcase.assertEqual("a", "a") # noqa: PT009 - - with pytest.raises(AssertionError): - django_testcase.assertXMLEqual("a" * 10_000, "a") - - class TestInternalDjangoAssert: def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: assert django_testcase != self @@ -102,6 +93,27 @@ def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: django_testcase.assertXMLEqual("a" * 10_000, "a") +@pytest.mark.django_project(create_manage_py=True) +def test_django_test_case_assert(django_pytester: DjangoPytester) -> None: + django_pytester.create_test_module( + """ + import django.test + + class TestDjangoAssert(django.test.TestCase): + def test_fixture_assert(self, django_testcase: unittest.TestCase) -> None: + assert False, "Cannot use the fixture" + + def test_normal_assert(self) -> None: + self.assertEqual("a", "a") + with pytest.raises(AssertionError): + self.assertXMLEqual("a" * 10_000, "a") + """ + ) + result = django_pytester.runpytest_subprocess() + result.assert_outcomes(failed=1, passed=1) + assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str() + + @pytest.mark.django_project(create_manage_py=True) def test_unittest_assert(django_pytester: DjangoPytester) -> None: django_pytester.create_test_module( @@ -110,7 +122,7 @@ def test_unittest_assert(django_pytester: DjangoPytester) -> None: class TestUnittestAssert(unittest.TestCase): def test_fixture_assert(self, django_testcase: unittest.TestCase) -> None: - assert False + assert False, "Cannot use the fixture" def test_normal_assert(self) -> None: self.assertEqual("a", "a") From 371ecfbdc356648805ccb2c50a535e7aa8626454 Mon Sep 17 00:00:00 2001 From: Javier Buzzi Date: Wed, 9 Apr 2025 10:03:09 -0400 Subject: [PATCH 32/33] Update test_asserts.py --- tests/test_asserts.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index 1cde54ba..c8a1b505 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -110,7 +110,7 @@ def test_normal_assert(self) -> None: """ ) result = django_pytester.runpytest_subprocess() - result.assert_outcomes(failed=1, passed=1) + result.assert_outcomes(errors=1, passed=1) assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str() From 81aaf828c30d43ddce063cd3242dbe0e36130416 Mon Sep 17 00:00:00 2001 From: kingbuzzman Date: Wed, 9 Apr 2025 10:10:08 -0400 Subject: [PATCH 33/33] Had to bring out the big guns.. --- tests/test_asserts.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/test_asserts.py b/tests/test_asserts.py index c8a1b505..2f4ed386 100644 --- a/tests/test_asserts.py +++ b/tests/test_asserts.py @@ -97,10 +97,11 @@ def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: def test_django_test_case_assert(django_pytester: DjangoPytester) -> None: django_pytester.create_test_module( """ + import pytest import django.test class TestDjangoAssert(django.test.TestCase): - def test_fixture_assert(self, django_testcase: unittest.TestCase) -> None: + def test_fixture_assert(self, django_testcase: django.test.TestCase) -> None: assert False, "Cannot use the fixture" def test_normal_assert(self) -> None: @@ -110,7 +111,7 @@ def test_normal_assert(self) -> None: """ ) result = django_pytester.runpytest_subprocess() - result.assert_outcomes(errors=1, passed=1) + result.assert_outcomes(failed=1, passed=1) assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str()