Skip to content

Commit

Permalink
Fix calling of setUpClass/tearDownClass in classes with multiple inhe…
Browse files Browse the repository at this point in the history
…ritance

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 pytest-dev#265.
  • Loading branch information
pelme committed Oct 4, 2015
1 parent 93451f1 commit 96f5fb1
Show file tree
Hide file tree
Showing 4 changed files with 125 additions and 12 deletions.
8 changes: 7 additions & 1 deletion docs/changelog.rst
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@ Bug fixes
^^^^^^^^^
* Ensure urlconf is properly reset when using @pytest.mark.urls. Thanks to
Sarah Bird, David Szotten, Daniel Hahler and Yannick PÉROUX for patch and
discussions.
discussions. Fixes `issue #183
<https://github.com/pytest-dev/pytest-django/issues/183>`_.

* Call `setUpClass()` in Django `TestCase` properly when test class is
inherited multiple places. Thanks to Benedikt Forchhammer for report and
initial test case. Fixes `issue
#265<https://github.com/pytest-dev/pytest-django/issues/265>`_.

Compatibility
^^^^^^^^^^^^^
Expand Down
64 changes: 53 additions & 11 deletions pytest_django/plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -230,19 +230,55 @@ def pytest_configure():
_setup_django()


def pytest_runtest_setup(item):
def _method_is_defined_at_leaf(cls, method_name):
return getattr(cls.__base__, method_name).__func__ is not getattr(cls, method_name).__func__

if django_settings_is_configured() and is_django_unittest(item):
cls = item.cls

if hasattr(cls, '__real_setUpClass'):
return
_disabled_classmethods = {}


def _disable_class_methods(cls):
if cls in _disabled_classmethods:
return

cls.__real_setUpClass = cls.setUpClass
cls.__real_tearDownClass = cls.tearDownClass
_disabled_classmethods[cls] = (
cls.setUpClass,
_method_is_defined_at_leaf(cls, 'setUpClass'),
cls.tearDownClass,
_method_is_defined_at_leaf(cls, 'tearDownClass'),
)

cls.setUpClass = types.MethodType(lambda cls: None, cls)
cls.tearDownClass = types.MethodType(lambda cls: None, cls)
cls.setUpClass = types.MethodType(lambda cls: None, cls)
cls.tearDownClass = types.MethodType(lambda cls: None, cls)


def _restore_class_methods(cls):
(setUpClass,
restore_setUpClass,
tearDownClass,
restore_tearDownClass) = _disabled_classmethods.pop(cls)

try:
del cls.setUpClass
except AttributeError:
raise

try:
del cls.tearDownClass
except AttributeError:
pass

if restore_setUpClass:
cls.setUpClass = setUpClass

if restore_tearDownClass:
cls.tearDownClass = tearDownClass


def pytest_runtest_setup(item):
if django_settings_is_configured() and is_django_unittest(item):
cls = item.cls
_disable_class_methods(cls)


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

_django_cursor_wrapper.enable()
request.node.cls.__real_setUpClass()

cls = request.node.cls

_restore_class_methods(cls)
cls.setUpClass()
_disable_class_methods(cls)

def teardown():
request.node.cls.__real_tearDownClass()
_restore_class_methods(cls)
cls.tearDownClass()
_django_cursor_wrapper.restore()

request.addfinalizer(teardown)
Expand Down
6 changes: 6 additions & 0 deletions pytest_django_test/app/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,9 @@

class Item(models.Model):
name = models.CharField(max_length=100)

def __unicode__(self):
return self.name

def __str__(self):
return self.name
59 changes: 59 additions & 0 deletions tests/test_unittest.py
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,65 @@ def test_pass(self):
])
assert result.ret == 0

def test_multi_inheritance_setUpClass(self, django_testdir):
django_testdir.create_test_module('''
from django.test import TestCase
from .app.models import Item
class TestA(TestCase):
expected_state = ['A']
state = []
@classmethod
def setUpClass(cls):
super(TestA, cls).setUpClass()
cls.state.append('A')
@classmethod
def tearDownClass(cls):
assert cls.state.pop() == 'A'
super(TestA, cls).tearDownClass()
def test_a(self):
assert self.state == self.expected_state
class TestB(TestA):
expected_state = ['A', 'B']
@classmethod
def setUpClass(cls):
super(TestB, cls).setUpClass()
cls.state.append('B')
@classmethod
def tearDownClass(cls):
assert cls.state.pop() == 'B'
super(TestB, cls).tearDownClass()
def test_b(self):
assert self.state == self.expected_state
class TestC(TestB):
expected_state = ['A', 'B', 'C']
@classmethod
def setUpClass(cls):
super(TestC, cls).setUpClass()
cls.state.append('C')
@classmethod
def tearDownClass(cls):
assert cls.state.pop() == 'C'
super(TestC, cls).tearDownClass()
def test_c(self):
assert self.state == self.expected_state
''')

result = django_testdir.runpytest_subprocess('-vvvv', '-s')
assert result.parseoutcomes()['passed'] == 6
assert result.ret == 0

def test_unittest(self, django_testdir):
django_testdir.create_test_module('''
from unittest import TestCase
Expand Down

0 comments on commit 96f5fb1

Please sign in to comment.