Search code examples
pythonpython-3.xunit-testingtestingmultiple-inheritance

Inherit unit test from parent class


I would like to write some test in a way that are executed for all classes that inherit from a Parent.

For example I have the class motor with two specializations:

class Motor():

    def run(self, energy):
        pass


class ElectricMotor(Motor):

    def run(self, electric_energy):
        heat = electric_energy * 0.99
        motion = electric_energy * 0.01
        return heat, motion

class DieselMotor(Motor):

    def run(self, diesel_energy):
        heat = diesel_energy * 0.65
        motion = diesel_energy * 0.35
        return heat, motion

Then I have two tests which apply to every kind of motor:

class MotorTest(unittest.TestCase):

    def test_energy_should_be_conserved():

        for class_instance in all_motor_child_classes:
            energy=10
            assert sum(class_instance.run(energy))==energy
            energy=20
            assert sum(class_instance.run(energy))==energy

    def test_motors_should_produce_heat():

        for class_instance in all_motor_child_classes:
            energy = 10
            heat, motion=class_instance.run(energy)
            assert heat>0

What I'm looking for is a way to do the loop

for class_instance in all_motor_child_classes:

or a different programming pattern to obtain the same result.

Any idea? Thanks Riccardo


Solution

  • Well, there are two points here: first having a list of Motor child classes, then having an instance of each of those classes.

    The stupid simple solution is to maintain those lists in your testcase's setUp :

    from motors import ElectricMotor, DieselMotor
    
    class MotorTest(unittest.TestCase):
    
        _MOTOR_CHILD_CLASSES = [ElectricMotor, DieselMotor]
    
        def setUp(self):
           self.motor_child_instances = [cls() for cls in self._MOTOR_CHILD_CLASSES]
    
        def test_energy_should_be_conserved():
            for class_instance in self.motor_child_instances:
                self.assertEqual(sum(class_instance.run(10)), 10)
                # etc
    

    If your Motor subclasses __init__() expect different arguments (which they shouldn't if you want to have proper subtyping according to liskov substitution principle - but well, "practicality beats purity"), you can add those arguments to your MOTOR_CHILD_CLASSES list:

       # (cls, args, kw) tuples
        _MOTOR_CHILD_CLASSES = [
           (ElectricMotor, (42,), {"battery":"ioncad"}),
           (DieselMotor, (), {"cylinders":6}),
           ]
    

    and use them in the setUp():

           self.motor_child_instances = [
               cls(*args, **kw) for cls, args, kw in self._MOTOR_CHILD_CLASSES
           ]
    

    For something more "automagic", you can use a custom metaclass on Motor so it can registers its subclasses and provide a list of them, but then you'll loose the ability to provide per-class arguments - and you'll also make your tests code much less readable and predicable.

    Now another - and IMHO much better - approach is to use inheritance in your tests instead: define a mixin object with all the tests that are common to all Motor child classes :

    class MotorTestMixin(object):
    
        # must be combined with a unittest.TestCase that
        # defines `self.instance` as a `Motor` subclass instance
    
        def test_energy_should_be_conserved(self):
            self.assertEqual(sum(self.instance.run(10)), 10)
    
        def test_should_produce_heat(self):
            heat, motion = self.instance.run(10)
            self.assertGreater(heat, 0)
    

    then have one TestCase per subclass:

    class DieselMotorTest(MotorTestMixin, TestCase):
        def setUp(self):
            self.instance = DieselMotor()
    
    
    class ElectricMotorTest(MotorTestMixin, TestCase):
        def setUp(self):
            self.instance = ElectricMotor()
    

    One of the benefits of this approach (the others being simplicity, readability, and a much better error reporting on failed tests - you'll immediatly know which subclass failed without having anything special to do) is that you don't have to touch your existing code when you add a new Motor subclass - you just need to add a new individual TestCase for it -, and you can even do so in a distinct module, following the open/closed principle.