From d8e3b50c35bb9d30db106619b03ab2a7ac115708 Mon Sep 17 00:00:00 2001 From: Paul Tiplady Date: Mon, 29 Nov 2021 15:37:11 -0800 Subject: [PATCH] Wire up cls.setUpTestData for class level fixtures Reimplement the Django TestCase's setUpTestData. Since we are already calling the setUpClass machinery from TestCase, it's a simple step to arrange for PytestDjangoTestCase to call the real test class's setUpTestData classmethod. This allows us to defer to the existing lifecycle hook machinery, and also use the Django 3.2 implementation for TestData which uses a descriptor to handle re-loading test instances between tests. (It does this by memoizing the pre-test instances, so this avoids us having to add a DB transaction or refresh_from_db() to reset the testData.) --- pytest_django/fixtures.py | 15 +++++++++++++++ tests/test_database.py | 18 ++++++++++++++++++ 2 files changed, 33 insertions(+) diff --git a/pytest_django/fixtures.py b/pytest_django/fixtures.py index 8c955858..24814a9a 100644 --- a/pytest_django/fixtures.py +++ b/pytest_django/fixtures.py @@ -182,6 +182,21 @@ class PytestDjangoTestCase(test_case_class): # type: ignore[misc,valid-type] if _databases is not None: databases = _databases + if VERSION >= (3, 2): + from django.test.testcases import TestData + # If the test class has defined a Django-style setUpTestData, + # arrange for it to be called by the DjangoTestCase classmethods that we're calling here. + if request.cls and hasattr(request.cls, 'setUpTestData'): + def wrappedSetUpTestData(klass): + pre_attrs = request.cls.__dict__.copy() + request.cls.setUpTestData() + for name, value in request.cls.__dict__.items(): + if value is not pre_attrs.get(name): + setattr(request.cls, name, TestData(name, value)) + + # Re-bind our new classmethod to the class we're attaching it to using the function's descriptor. + PytestDjangoTestCase.setUpTestData = wrappedSetUpTestData.__get__(PytestDjangoTestCase, PytestDjangoTestCase) + PytestDjangoTestCase.setUpClass() if VERSION >= (4, 0): request.addfinalizer(PytestDjangoTestCase.doClassCleanups) diff --git a/tests/test_database.py b/tests/test_database.py index 9016240b..52b05ebb 100644 --- a/tests/test_database.py +++ b/tests/test_database.py @@ -343,3 +343,21 @@ def test_db_access_in_test_module(self, django_testdir) -> None: 'or the "db" or "transactional_db" fixtures to enable it.' ] ) + + +# TODO: If this is omitted, the error is confusing: +# AttributeError: 'TestDatabaseClassAutoCaching' object has no attribute 'item_1' +@pytest.mark.django_db +class TestDatabaseClassAutoCaching: + + @classmethod + def setUpTestData(cls): + cls.item_1 = Item.objects.create() + + # But it's not idiomatic for pytest: + def test_foo_c(self): + self.item_1.name = 'foo' + self.item_1.save() + + def test_bar_c(self): + assert not self.item_1.name