MattSom
MattSom

Reputation: 2377

How to revert Mocking

I almost went mad by the time I figured out why my test file runs smoothly but breaks if tests are run together for the whole package with pytest.

I have a parent class and a child class. The child calls the parent's init like:

class Child(Parent):
    __init__(...,**kwargs):
        ...
        super().__init__(**kwargs)

I wanted to define this behavior with a tiny test.

from unittest.mock import Mock

def test_init_calls_parent_init():
    Parent.__init__ = Mock()
    
    Child()

    assert Parent.__init__.called

The problem is Parent.__init__ remained persistently a mock for all the following tests in other files.

I had the notion that putting it into a function scope makes it only a temporary change. Of course since those tests broke, they implicitly define the need for the parent init but I wanted to make sure with one explicit test as well.

Should I create some pytest setup/teardown or what is the accepted way to prevent this?

Upvotes: 3

Views: 1354

Answers (2)

Niel Godfrey P. Ponciano
Niel Godfrey P. Ponciano

Reputation: 10709

When you performed Parent.__init__ = Mock(), you basically redefined the __init__ of the module itself, which then reflected on the succeeding tests.

Instead of manually changing the implementation of Parent.__init__ to Mock, my suggestion is to just use instead the patching functionality already available in unittest and pytest-mock.

  • We can also use monkeypatch as suggested by @MattSom (see comments section)

src.py

class Parent:
    def __init__(self, **kwargs):
        print("Parent __init__ called")

class Child(Parent):
    def __init__(self, **kwargs):
        super().__init__(**kwargs)
        print("Child __init__ called")

Without the correction:

from unittest.mock import Mock, patch

from src import Child, Parent


def test_init_calls_real_parent_init():
    Child()


def test_init_calls_updated_parent_init():
    Parent.__init__ = Mock()

    Child()

    assert Parent.__init__.called


def test_init_calls_real_parent_init_2():
    Child()

Output:

$ pytest -q test_src.py -rP
...                                                                                                                                                                                                 [100%]
================================================================================================= PASSES ==================================================================================================
____________________________________________________________________________________ test_init_calls_real_parent_init _____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
___________________________________________________________________________________ test_init_calls_updated_parent_init ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_real_parent_init_2 ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
3 passed in 0.01s

Findings:

The 1st test called the real Parent.__init__. The 2nd test called the mock. During the 3rd test however, it also unexpectedly called the mock made in the 2nd test.

With correction:

from unittest.mock import Mock, patch

from src import Child, Parent


def test_init_calls_real_parent_init():
    Child()


# Personally I wouldn't advise to do this. It just works :)
def test_init_calls_updated_parent_init():
    # Setup
    orig_parent_init = Parent.__init__  # Store original init

    # Real test
    Parent.__init__ = Mock()

    Child()

    assert Parent.__init__.called

    # Teardown
    Parent.__init__ = orig_parent_init  # Bring back the original init


@patch("src.Parent.__init__")  # Uses unittest
def test_init_calls_mocked_parent_init(mock_parent_init):
    Child()

    assert mock_parent_init.called


def test_init_calls_mocked_parent_init_2(mocker):  # Uses pytest-mock
    mock_parent_init = mocker.patch("src.Parent.__init__")

    Child()

    assert mock_parent_init.called


def test_init_calls_real_parent_init_2():
    Child()

Output:

$ pytest -q test_src_2.py -rP
.....                                                                                                                                                                                               [100%]
================================================================================================= PASSES ==================================================================================================
____________________________________________________________________________________ test_init_calls_real_parent_init _____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
___________________________________________________________________________________ test_init_calls_updated_parent_init ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_mocked_parent_init ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
__________________________________________________________________________________ test_init_calls_mocked_parent_init_2 ___________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Child __init__ called
___________________________________________________________________________________ test_init_calls_real_parent_init_2 ____________________________________________________________________________________
------------------------------------------------------------------------------------------ Captured stdout call -------------------------------------------------------------------------------------------
Parent __init__ called
Child __init__ called
5 passed in 0.03s

Findings:

Here I used 2 solutions:

  1. Either change back the original implementation of Parent.__init__ after the manual reassignment to mock (see test_init_calls_updated_parent_init) (not advisable)
  2. Or use the builtin patching abilities of unittest and pytest-mock (see test_init_calls_mocked_parent_init and test_init_calls_mocked_parent_init_2)

Now, both the first and the last test correctly calls the actual Parent.__init__, even after all the mocks made.

Upvotes: 2

Chris
Chris

Reputation: 184

You can try converting the parent into a fixture, which defaults to having a "function" scope, which theoretically should teardown the fixture after each test.

Remember to yield your fixture and add any other necessary teardown instructions afterwards.

The reason your Parent does not deconstruct the mock is because parent classes/functions that aren't fixtures are treated like any other parent/child functions.

Upvotes: 1

Related Questions