Skip to content

Exposes the underlining TestCase instance to avoid using "asserts" module #1191

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 34 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
c56c818
Fixes issue with maxDiff and verbosity
kingbuzzman Apr 6, 2025
c551601
Update plugin.py
kingbuzzman Apr 6, 2025
f4d6ea9
Adds tests
kingbuzzman Apr 6, 2025
0a6ef42
Minor adjustment
kingbuzzman Apr 6, 2025
8b26d9b
Better tests
kingbuzzman Apr 6, 2025
3962bd5
Update test_asserts.py
kingbuzzman Apr 6, 2025
3520685
Update test_asserts.py
kingbuzzman Apr 6, 2025
0c796b9
Update test_asserts.py
kingbuzzman Apr 6, 2025
c2d9f72
Update test_asserts.py
kingbuzzman Apr 6, 2025
14ce12a
Update test_asserts.py
kingbuzzman Apr 6, 2025
567fa23
Update test_asserts.py
kingbuzzman Apr 6, 2025
d42d733
Update test_asserts.py
kingbuzzman Apr 6, 2025
e705113
Merge branch 'main' into patch-5
kingbuzzman Apr 7, 2025
4917b34
does this work??
kingbuzzman Apr 7, 2025
90d40b2
minor progress
kingbuzzman Apr 7, 2025
636771b
.
kingbuzzman Apr 7, 2025
60578dc
.
kingbuzzman Apr 7, 2025
8ee49d0
Fixes test
kingbuzzman Apr 7, 2025
5da273a
Removes maxDiff for now
kingbuzzman Apr 7, 2025
ccbead4
Minor updates
kingbuzzman Apr 7, 2025
a91d2ce
Fixes tests
kingbuzzman Apr 7, 2025
1130721
Minor rename
kingbuzzman Apr 7, 2025
91c867b
Adds docs
kingbuzzman Apr 7, 2025
c8fccc0
Minor
kingbuzzman Apr 7, 2025
b75db55
.
kingbuzzman Apr 7, 2025
3b2e45f
oops
kingbuzzman Apr 7, 2025
26e6ebc
Update test_asserts.py
kingbuzzman Apr 9, 2025
48b0320
Update test_asserts.py
kingbuzzman Apr 9, 2025
8f36bd8
Update test_asserts.py
kingbuzzman Apr 9, 2025
8f086dc
Update test_asserts.py
kingbuzzman Apr 9, 2025
4efe1a4
Update test_asserts.py
kingbuzzman Apr 9, 2025
48a658e
Update test_asserts.py
kingbuzzman Apr 9, 2025
371ecfb
Update test_asserts.py
kingbuzzman Apr 9, 2025
81aaf82
Had to bring out the big guns..
kingbuzzman Apr 9, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 22 additions & 5 deletions docs/helpers.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
-------
Expand Down Expand Up @@ -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)
Expand Down
10 changes: 10 additions & 0 deletions pytest_django/asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from __future__ import annotations

import warnings
from functools import wraps
from typing import TYPE_CHECKING, Any, Callable, Sequence

Expand All @@ -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
Expand Down
126 changes: 72 additions & 54 deletions pytest_django/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@
"django_db_reset_sequences",
"django_db_serialized_rollback",
"django_db_setup",
"django_testcase",
"django_testcase_class",
"django_user_model",
"django_username_field",
"live_server",
Expand Down Expand Up @@ -216,15 +218,15 @@ def django_db_setup(


@pytest.fixture()
def _django_db_helper(
def django_testcase_class(
request: pytest.FixtureRequest,
django_db_setup: None,
django_db_blocker: DjangoDbBlocker,
) -> Generator[None, None, None]:
) -> Generator[type[django.test.TestCase] | None, None, None]:
if is_django_unittest(request):
yield
yield None
return

import django.test

marker = request.node.get_closest_marker("django_db")
if marker:
(
Expand Down Expand Up @@ -253,63 +255,74 @@ 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()

yield PytestDjangoTestCase


@pytest.fixture()
def _django_db_helper(
request: pytest.FixtureRequest,
django_db_setup: None,
django_db_blocker: DjangoDbBlocker,
django_testcase_class: type[django.test.TestCase],
) -> Generator[None, None, None]:
if is_django_unittest(request):
yield
return

with django_db_blocker.unblock():
import django.db
import django.test
django_testcase_class.setUpClass()

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.setUpClass()

test_case = PytestDjangoTestCase(methodName="__init__")
test_case = django_testcase_class(methodName="__init__")
test_case._pre_setup()

yield
yield test_case

test_case._post_teardown()

PytestDjangoTestCase.tearDownClass()
django_testcase_class.tearDownClass()

PytestDjangoTestCase.doClassCleanups()
django_testcase_class.doClassCleanups()


def _django_db_signature(
Expand Down Expand Up @@ -379,6 +392,11 @@ def _set_suffix_to_test_databases(suffix: str) -> None:
# ############### User visible fixtures ################


@pytest.fixture()
def django_testcase(_django_db_helper: django.test.TestCase | None) -> django.test.TestCase | None:
return _django_db_helper


@pytest.fixture()
def db(_django_db_helper: None) -> None:
"""Require a django test database.
Expand Down
2 changes: 2 additions & 0 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@
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
Expand Down
61 changes: 61 additions & 0 deletions tests/test_asserts.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,11 @@

import inspect

import django.test
import pytest

from .helpers import DjangoPytester

import pytest_django
from pytest_django.asserts import __all__ as asserts_all

Expand Down Expand Up @@ -71,3 +74,61 @@ def test_sanity() -> None:
pass

assert assertContains.__doc__


def test_fixture_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_fixture_assert(self, django_testcase: django.test.TestCase) -> None:
assert django_testcase != self
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_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: django.test.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(
"""
import unittest

class TestUnittestAssert(unittest.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")
"""
)
result = django_pytester.runpytest_subprocess()
result.assert_outcomes(failed=1, passed=1)
assert "missing 1 required positional argument: 'django_testcase'" in result.stdout.str()