8

When there is significant overlap in test setup, it can keep things DRY to use inheritance. But this causes issues with unnecessary duplication of test execution:

from unittest import TestCase

class TestPotato(TestCase):
    def test_in_parent(self):
        print 'in parent'

class TestSpud(TestPotato):
    def test_in_child(self):
        print 'in child'

Testing this module runs the test_in_parent twice.

$ python -m unittest example
in parent
.in child
.in parent
.
----------------------------------------------------------------------
Ran 3 tests in 0.000s

OK

Why? Is this by design? Can it be disabled by configuring the test runner in a certain way?

I can workaround the issue by moving setup into a non-discovered class, and then use multiple inheritence, but it seems a bit hacky and unnecessary.

note: Same problem occurs in other runners such as nose (nosetests -s example.py) and pytest (py.test example.py)

wim
  • 338,267
  • 99
  • 616
  • 750
  • 3
    Because they inherit the test methods of their parents, so when `unittest` looks through their `dir` (or `__dict__` or whatever it does) for methods starting `test_` it finds the inherited ones, too. I don't think solving this requires *multiple* inheritance; abstract what both need to a third, undiscoverable class with no `test_` methods and have them both inherit it. – jonrsharpe Nov 03 '15 at 15:50
  • Introspecting a subclass that has a parent class that contains methods prefixed by test will show the subclass with those methods. This is python OOP. I don't think moving setup methods to mixin or as a separate base class seems hacky at all, and is maybe DRYer – dm03514 Nov 03 '15 at 15:51
  • Seems like a great use case for mixins for different "groups" of tests to dynamically create the exact test you need...tree-inheritance doesn't look like the right model here. – Shashank Nov 03 '15 at 15:53

3 Answers3

6

Test runners look up of all methods starting with test. Inherited methods are present in child class - therefore they are detected as tests to run. To avoid that you should extract common code in parent class and do not inherit any actual tests.

from unittest import TestCase

class PotatoTestTemplate(TestCase):
    def setUp():
        pass

class PotatoTest1(PotatoTestTemplate):
    def test1(self):
        pass

class PotatoTest2(PotatoTestTemplate):
    def test1(self):
        pass
Łukasz Rogalski
  • 22,092
  • 8
  • 59
  • 93
  • I have settled on a pattern similar to this - instead of the `PotatoTestTemplate` I use a `PotatoSetup(object)`. It's not a `TestCase`, it's more like a mixin. Then I use `PotatoTest(PotatoSetup, TestCase)` and `SpudTest(PotatoSetup, TestCase)` with extra setup in either test if I need it. – wim Oct 27 '16 at 00:06
1

Another workaround i have seen people use is that nested classes wont get run as part of nosetests e.g.

from unittest import TestCase
class NotTested:
    class TestPotato(TestCase):
        def test_in_parent(self):
            print 'in parent'

class TestSpud(NotTested.TestPotato):
    def test_in_child(self):
        print 'in child'

A workaround I unsuccessfully tried was to use multiple inheritance so the TestPotato class extends object and TestSpud does extend from TestCase and TestPotato e.g.

from unittest import TestCase

class TestPotato(object):
    def test_in_parent(self): 
        # still gets ran twice because
        # nosetests runs anything that starts with test_* :(
         print 'in parent'

class TestSpud(TestCase, TestPotato):
    def test_in_child(self):
        print 'in child'

But this actually didn't work for me, i wish it did because you wouldn't need the added nesting of code... but it looks like using multiple inheritance is bad anyway

lee penkman
  • 1,160
  • 15
  • 19
1

If the test setUp from a different test class is all you need, you could do this:

from unittest import TestCase

class TestPotato(TestCase):
    def setUp(self):
        print('fixtures here')

    def test_in_parent(self):
        print 'in parent'


class TestSpud(TestCase):
    def setUp(self):
        TestPotato.setUp(self)

    def test_in_child(self):
        print 'in child'
Larry
  • 52
  • 1
  • 8