0

What I want to do

I have a class with multiple inheritance for which I want to prepare unit test.

Why I want to do this

Assume that I write some program where I have both animals and plants in there. I have many various Animals (so many Animals subclasses), same with plants. This is why I need both animal and plant type. There are many animals and plants species, this is why I have many inheritence in both animals and plants world. Unfortunately, I have one type that is both some plant and animal (e.g. Lion with Oak like attributes - classes A i B in the example). What I would like to do is to mix both plant and animal attributes and methods into just this one type (class C). I know that it sounds ridiculous, but this is my exact goal and I expect mistakes there as it is quite special. This is why I want to have proper testing there so no mistakes are introduced.

First idea

Let's assume that my code looks like this (I am open for C.__init__ refinement suggestions):
inheritance.py

class A:
    def __init__(self, p1):
        self.p1 = p1


class B:
    def __init__(self, p2):
        self.p2 = p2


class C(A, B):
    def __init__(self, p1, p2):
        # I want to execute init methods of both A and B.
        A.__init__(self=self, p1=p1)
        B.__init__(self=self, p2=p2)

My first idea for unit tests were like this:
test_inheritance.py

import pytest
from mock import Mock, patch

from inheritance import C


class TestC:
    def setup(self):
        self.mock_c_object = Mock(spec=C)
        # patching A.__init__ and B.__init__
        self._patcher_a_init_ = patch("inheritance.A.__init__")
        self.mock_a_init = self._patcher_a_init_.start()
        self._patcher_b_init_ = patch("inheritance.B.__init__")
        self.mock_b_init = self._patcher_b_init_.start()

    def teardown(self):
        self._patcher_a_init_.stop()
        self._patcher_b_init_.stop()

    @pytest.mark.parametrize("p1", [0, 1])
    @pytest.mark.parametrize("p2", ["x", "y"])
    def test_c_init(self, p1, p2):
        C.__init__(self=self.mock_c_object, p1=p1, p2=p2)
        self.mock_a_init.assert_called_once_with(self=self.mock_c_object, p1=p1)
        self.mock_b_init.assert_called_once_with(self=self.mock_c_object, p2=p2)

What is the problem

When I tried to run this test, I faced this traceback:

test_inheritance.py:19 (TestC.test_c_init[x-0])
self = <test_inheritance.TestC object at 0x042C0520>, p1 = 0, p2 = 'x'

    @pytest.mark.parametrize("p1", [0, 1])
    @pytest.mark.parametrize("p2", ["x", "y"])
    def test_c_init(self, p1, p2):
>       C.__init__(self=self.mock_c_object, p1=p1, p2=2)

test_inheritance.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
inheritance.py:15: in __init__
    A.__init__(self=self, p1=p1)
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

_mock_self = <MagicMock name='__init__' id='70100424'>, args = ()
kwargs = {'p1': 0, 'self': <Mock spec='C' id='69801632'>}

    def __call__(_mock_self, *args, **kwargs):
        # can't use self in-case a function / method we are mocking uses self
        # in the signature
>       _mock_self._mock_check_sig(*args, **kwargs)
E       TypeError: _mock_check_sig() got multiple values for argument 'self'

C:\Python38\lib\site-packages\mock\mock.py:1098: TypeError

How I tried to fix this

I suppose in order to fix the problem with mock (multiple self signatures), I have to start using super in C.__init__.
I tried to make such update in inheritance.py:

class C(A, B):
    def __init__(self, p1, p2):
        super(B, C).__init__(p1=p1)
        super(A, C).__init__(p2=p2)

Unfortunately, something is wrong with my code:

test_inheritance.py:19 (TestC.test_c_init[x-0])
self = <test_inheritance.TestC object at 0x0414E268>, p1 = 0, p2 = 'x'

    @pytest.mark.parametrize("p1", [0, 1])
    @pytest.mark.parametrize("p2", ["x", "y"])
    def test_c_init(self, p1, p2):
>       C.__init__(self=self.mock_c_object, p1=p1, p2=2)

test_inheritance.py:23: 
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <Mock spec='C' id='68478408'>, p1 = 0, p2 = 2

    def __init__(self, p1, p2):
>       super(B, C).__init__(p1=p1)
E       TypeError: descriptor '__init__' of 'object' object needs an argument

inheritance.py:13: TypeError

Could you help me please? Honestly, I have only used super for single inheritance till now, therefore I have not had problems like this one. Thank you in advance.

Maciek
  • 463
  • 8
  • 22
  • Why are you trying to mock anything here? Note for multiple inheritance to work in cases like this you really need to be consistently using `super`, `*args` and `**kwargs`. – jonrsharpe Jan 23 '21 at 10:19
  • See e.g. https://stackoverflow.com/q/3277367/3001761, https://stackoverflow.com/q/34884567/3001761 – jonrsharpe Jan 23 '21 at 10:29
  • @jonrsharpe I am mocking things to prepare unit test as this `C.__init__` is doing a few things and this is crucial to make sure that both `A.__init__` and `B.__init__` are properly called (with proper arguments).. – Maciek Jan 24 '21 at 08:30
  • Test *behaviour*, not *implementation*. Test that an instance of `C` works correctly, has the right attributes, not the specific calls it makes. Tests should give you confidence your code works in actual use and support refactoring; what you've posted below, which calls magic methods directly and is heavily coupled to implementation specifics, does neither. – jonrsharpe Jan 24 '21 at 08:36
  • @jonrsharpe The example is simplified to the maximum. Actually, `A.__init__` and `B.__init__` are more complecated and both are inheriting after (much) different classes. I have added 'Why I want to do this' description, so everyone could understand it better. I know this might be strange to you, but I would appreciate help how to solve the exact problem that I posted. Thank you in advance. – Maciek Jan 24 '21 at 08:56
  • It would be helpful to give a less abstract example than ABC (mixing plants and animals - Venus flytrap maybe?) but the point remains that you don't test that by mocking out what it inherits, because that's *part of the thing you're supposed to be testing*. Mocks are for *collaborators*. Your test should be `c = C(p1, p2)` followed by assertions on `c.p1 == p1` and `c.p2 = p2`. As long as the instance meets those behaviour requirements, the implementation details of how it achieves them don't matter. – jonrsharpe Jan 24 '21 at 10:18

1 Answers1

-1

First working solution I have managed so far:
inheritance.py

class A:
    def __init__(self_, p1):
        self_.p1 = p1


class B:
    def __init__(self_, p2):
        self_.p2 = p2


class C(A, B):
    def __init__(self, p1, p2):
        # I want to execute init methods of both A and B.
        A.__init__(self_=self, p1=p1)
        B.__init__(self_=self, p2=p2)

test_inheritance.py

import pytest
from mock import MagicMock, patch

from inheritance import C


class TestC:
    def setup(self):
        self.mock_c_object = MagicMock(spec=C)
        # patching A.__init__ and B.__init__
        self._patcher_a_init_ = patch("inheritance.A.__init__")
        self.mock_a_init = self._patcher_a_init_.start()
        self._patcher_b_init_ = patch("inheritance.B.__init__")
        self.mock_b_init = self._patcher_b_init_.start()

    def teardown(self):
        self._patcher_a_init_.stop()
        self._patcher_b_init_.stop()

    @pytest.mark.parametrize("p1", [0, 1])
    @pytest.mark.parametrize("p2", ["x", "y"])
    def test_c_init(self, p1, p2):
        C.__init__(self=self.mock_c_object, p1=p1, p2=p2)
        self.mock_a_init.assert_called_once_with(self_=self.mock_c_object, p1=p1)
        self.mock_b_init.assert_called_once_with(self_=self.mock_c_object, p2=p2)

Do you have any other proposals?

Maciek
  • 463
  • 8
  • 22