thehale
thehale

Reputation: 1766

How to mock a constructor called in a parent class while testing a child class using Python

I'm trying to mock an object that makes some expensive network calls when it's initialized, but the parent class instantiating it isn't recognizing the patch from my unit test.

Some similar questions I've already looked at:

Minimal Reproducible Example 3

File Structure
--src
   |-- resource.py
   |-- family.py
   |-- test_child.py
resource.py
class Resource:

    def __init__(self):
        print("Expensive network call")

    def use_resource(self) -> str:
        print("Another expensive network call")
        return "bar"
family.py
from resource import Resource

class Parent:

    def __init__(self):
        print("In the Parent constructor.")
        self.resource = Resource()

    def foo(self):
        print("Parent's foo")
        print(self.resource.use_resource())

class Child(Parent):

    def __init__(self):
        super().__init__()
        print("In the Child constructor")
    
    def foo(self):
        print("Child's foo")
        super().foo()
test_child.py
import unittest
from unittest.mock import patch

from family import Child

class TestChild(unittest.TestCase):

    @patch('resource.Resource')
    def test_foo(self, mock_resource):
        mock_resource.return_value.use_resource.return_value = "mock_bar"
        child = Child()
        child.foo()
        mock_resource.return_value.use_resource.assert_called_once()

unittest.main()

Expected Result

Since I'm patching resource.Resource, I'm expecting to avoid the expensive network calls that normally occur when a Parent instantiates a Resource. Theoretically the output of running test_child.py should look like this:

In the Parent constructor.
In the Child constructor
Child's foo
Parent's foo
mock_bar

Actual Result

However, despite patching resource.Resource in the test, the Parent is still instantiating an actual Resource as evidenced by the presence of the "Expensive network call" messages in the output and the failed assert_called_once assertion.

In the Parent constructor.
Expensive network call           # Should have been patched
In the Child constructor
Child's foo
Parent's foo
Another expensive network call   # Should have been patched
bar                              # Should have been patched
F
======================================================================
FAIL: test_foo (__main__.TestChild)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "[REMOVED]\unittest\mock.py", line 1342, in patched
    return func(*newargs, **newkeywargs)
  File "[REMOVED]\test_child.py", line 13, in test_foo
    mock_resource.return_value.use_resource.assert_called_once()
  File "[REMOVED]\unittest\mock.py", line 886, in assert_called_once
    raise AssertionError(msg)
AssertionError: Expected 'use_resource' to have been called once. Called 0 times.

----------------------------------------------------------------------
Ran 1 test in 0.005s

What do I need to change so that Parent's instantiation of a Resource uses a mocked Resource instead of an actual Resource?

Upvotes: 1

Views: 1606

Answers (1)

thehale
thehale

Reputation: 1766

Use @patch(family.Resource) instead of @patch(resource.Resource)

There are two key ideas that explain why your patch failed:

  1. When your Parent class instantiates a Resource, it is not directly instantiating a resource.Resource, rather it is instantiating the family module's import of a Resource.
  2. While your family module does import resource.Resource (which you are indeed patching), that import occurs prior to your patch because you import the family module prior to executing the patch.

See the unittest.mock documentation if you want more details. This article from Medium is also really helpful for understanding the oddities of mocking in Python.


Demonstration

For completeness, here's your test_child.py with the correction...

import unittest
from unittest.mock import patch

from family import Child


class TestChild(unittest.TestCase):

    @patch('family.Resource')  # Changed this line
    def test_foo(self, mock_resource):
        mock_resource.return_value.use_resource.return_value = "mock_bar"
        # Alternatively, move `from family import Child` here.
        child = Child()
        child.foo()
        mock_resource.return_value.use_resource.assert_called_once()

unittest.main()

... and the test output matches the expected output in your question.

In the Parent constructor.
In the Child constructor
Child's foo
Parent's foo
mock_bar
.
----------------------------------------------------------------------
Ran 1 test in 0.004s

OK

Upvotes: 1

Related Questions