Reputation: 2377
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
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.
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")
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.
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:
Parent.__init__
after the manual reassignment to mock (see test_init_calls_updated_parent_init
) (not advisable)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
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