Skip to content

Commit 96f5fb1

Browse files
committed
Fix calling of setUpClass/tearDownClass in classes with multiple inheritance
pytest-django monkeypatches Django's setUpClass / tearDownClass to call them at the correct time during fixture setup/teardown. The previous implementation caused problems when used with multiple inheritance. This commit fixes issue #265.
1 parent 93451f1 commit 96f5fb1

File tree

4 files changed

+125
-12
lines changed

4 files changed

+125
-12
lines changed

docs/changelog.rst

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,13 @@ Bug fixes
88
^^^^^^^^^
99
* Ensure urlconf is properly reset when using @pytest.mark.urls. Thanks to
1010
Sarah Bird, David Szotten, Daniel Hahler and Yannick PÉROUX for patch and
11-
discussions.
11+
discussions. Fixes `issue #183
12+
<https://github.com/pytest-dev/pytest-django/issues/183>`_.
13+
14+
* Call `setUpClass()` in Django `TestCase` properly when test class is
15+
inherited multiple places. Thanks to Benedikt Forchhammer for report and
16+
initial test case. Fixes `issue
17+
#265<https://github.com/pytest-dev/pytest-django/issues/265>`_.
1218

1319
Compatibility
1420
^^^^^^^^^^^^^

pytest_django/plugin.py

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -230,19 +230,55 @@ def pytest_configure():
230230
_setup_django()
231231

232232

233-
def pytest_runtest_setup(item):
233+
def _method_is_defined_at_leaf(cls, method_name):
234+
return getattr(cls.__base__, method_name).__func__ is not getattr(cls, method_name).__func__
234235

235-
if django_settings_is_configured() and is_django_unittest(item):
236-
cls = item.cls
237236

238-
if hasattr(cls, '__real_setUpClass'):
239-
return
237+
_disabled_classmethods = {}
238+
239+
240+
def _disable_class_methods(cls):
241+
if cls in _disabled_classmethods:
242+
return
240243

241-
cls.__real_setUpClass = cls.setUpClass
242-
cls.__real_tearDownClass = cls.tearDownClass
244+
_disabled_classmethods[cls] = (
245+
cls.setUpClass,
246+
_method_is_defined_at_leaf(cls, 'setUpClass'),
247+
cls.tearDownClass,
248+
_method_is_defined_at_leaf(cls, 'tearDownClass'),
249+
)
243250

244-
cls.setUpClass = types.MethodType(lambda cls: None, cls)
245-
cls.tearDownClass = types.MethodType(lambda cls: None, cls)
251+
cls.setUpClass = types.MethodType(lambda cls: None, cls)
252+
cls.tearDownClass = types.MethodType(lambda cls: None, cls)
253+
254+
255+
def _restore_class_methods(cls):
256+
(setUpClass,
257+
restore_setUpClass,
258+
tearDownClass,
259+
restore_tearDownClass) = _disabled_classmethods.pop(cls)
260+
261+
try:
262+
del cls.setUpClass
263+
except AttributeError:
264+
raise
265+
266+
try:
267+
del cls.tearDownClass
268+
except AttributeError:
269+
pass
270+
271+
if restore_setUpClass:
272+
cls.setUpClass = setUpClass
273+
274+
if restore_tearDownClass:
275+
cls.tearDownClass = tearDownClass
276+
277+
278+
def pytest_runtest_setup(item):
279+
if django_settings_is_configured() and is_django_unittest(item):
280+
cls = item.cls
281+
_disable_class_methods(cls)
246282

247283

248284
@pytest.fixture(autouse=True, scope='session')
@@ -315,10 +351,16 @@ def _django_setup_unittest(request, _django_cursor_wrapper):
315351
request.getfuncargvalue('_django_db_setup')
316352

317353
_django_cursor_wrapper.enable()
318-
request.node.cls.__real_setUpClass()
354+
355+
cls = request.node.cls
356+
357+
_restore_class_methods(cls)
358+
cls.setUpClass()
359+
_disable_class_methods(cls)
319360

320361
def teardown():
321-
request.node.cls.__real_tearDownClass()
362+
_restore_class_methods(cls)
363+
cls.tearDownClass()
322364
_django_cursor_wrapper.restore()
323365

324366
request.addfinalizer(teardown)

pytest_django_test/app/models.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,3 +3,9 @@
33

44
class Item(models.Model):
55
name = models.CharField(max_length=100)
6+
7+
def __unicode__(self):
8+
return self.name
9+
10+
def __str__(self):
11+
return self.name

tests/test_unittest.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,65 @@ def test_pass(self):
119119
])
120120
assert result.ret == 0
121121

122+
def test_multi_inheritance_setUpClass(self, django_testdir):
123+
django_testdir.create_test_module('''
124+
from django.test import TestCase
125+
from .app.models import Item
126+
127+
class TestA(TestCase):
128+
expected_state = ['A']
129+
state = []
130+
131+
@classmethod
132+
def setUpClass(cls):
133+
super(TestA, cls).setUpClass()
134+
cls.state.append('A')
135+
136+
@classmethod
137+
def tearDownClass(cls):
138+
assert cls.state.pop() == 'A'
139+
super(TestA, cls).tearDownClass()
140+
141+
def test_a(self):
142+
assert self.state == self.expected_state
143+
144+
class TestB(TestA):
145+
expected_state = ['A', 'B']
146+
147+
@classmethod
148+
def setUpClass(cls):
149+
super(TestB, cls).setUpClass()
150+
cls.state.append('B')
151+
152+
@classmethod
153+
def tearDownClass(cls):
154+
assert cls.state.pop() == 'B'
155+
super(TestB, cls).tearDownClass()
156+
157+
def test_b(self):
158+
assert self.state == self.expected_state
159+
160+
class TestC(TestB):
161+
expected_state = ['A', 'B', 'C']
162+
163+
@classmethod
164+
def setUpClass(cls):
165+
super(TestC, cls).setUpClass()
166+
cls.state.append('C')
167+
168+
@classmethod
169+
def tearDownClass(cls):
170+
assert cls.state.pop() == 'C'
171+
super(TestC, cls).tearDownClass()
172+
173+
def test_c(self):
174+
assert self.state == self.expected_state
175+
''')
176+
177+
result = django_testdir.runpytest_subprocess('-vvvv', '-s')
178+
assert result.parseoutcomes()['passed'] == 6
179+
assert result.ret == 0
180+
122181
def test_unittest(self, django_testdir):
123182
django_testdir.create_test_module('''
124183
from unittest import TestCase

0 commit comments

Comments
 (0)