Jacob R
Jacob R

Reputation: 327

Patching a function from within the same package is not patching

I am using the builtin unittest library. I am trying to test a function,in this example a, which calls function b. Both of these functions are at the top level of the package and are not class based functions. I have read the unittest documentation about where to patch and have found that you must use the patch against the function (a) importing the other function (b)and not patching the function itself (b), but all the answers and documentation seem to only reference class based functions.

I have the following heirarchy:

mypkg
|- __init__.py
|- a.py
|- b.py
|- test
  |- __init__.py
  |- test_a.py

top level __init__.py:

from .a import a
from .b import b
__all__ = ['a','b']

a.py:

import .b import b

def a():
  ...
  functionB({"USERNAME": username, "OTHER": other})

b.py:

def b(**kwargs):
 // do stuff with kwargs like calling APIs etc
 // This function doesn't return anything

test_a.py:

import unittest
from unittest.mock import patch
from ..a import a

fake_return_value = "heythere"

class TestFunctionA(unittest.TestCase):
 @patch('mypkg.a.b', return_value=fake_return_value)
 # @patch('mympkg.b') # Doesn't work, does not override the original function
 def test_mytest(self, mocked_func):
   a()
   mocked_func.assert_called_once_with(stuff)

Running the above tests with the patch call that is uncommented leads to the test being ran and the original function being called, not the mock function.

What am I doing wrong here? I didn't think it would be this hard to unit test, I come from unit testing React apps, so this is a little frustrating but I assume user error. All the documentation and googling I've done so far seems that this should work, or at least the patch variant commented out above. Is this because of my package structure and trying to mock functions in the same package or?

Upvotes: 0

Views: 1028

Answers (2)

thehale
thehale

Reputation: 1736

Let's look at your top level __init__.py which contains code executed at import time.

The statement from .a import a rebinds the name a from the module a, to the function a contained within module a. As such, all invocations of import mypkg.a (e.g. the patch in your test module) will return the function a and not the module. That is why you get the error AttributeError: function a does not have the attribute 'b' mentioned in your comment on my last answer.

To patch module a's usage of function b, your testing code needs to be able to import module a. There are a few ways you can do this.

  • Remove from .a import a in your __init__.py.
    • You can still access the module a via from mypkg import a or import mypkg.a
    • You can still access the function a via from mypkg.a import a or import mypkg.a.a
  • Rename the function a to something else.

Upvotes: 2

thehale
thehale

Reputation: 1736

NOTE: This answer addresses a previous revision of the question

The commented out patch is indeed the correct one to use since you are trying to patch a's reference to b. It doesn't really matter if b is a class or a function since, like JavaScript, both are first-class objects in Python and are looked up and patched by unittest.mock.patch in the same way.

I suspect the issue in your more complex system has to do with one of the following errors in your example:

  • patch should be imported as from unittest.mock import patch.
  • The mocked return_value should be assigned within your test, not within the decorator.
  • stuff isn't in the scope of your test class.

Working, Equivalent Example

Here's a working example of the system you describe:

  • Two modules, a and b, each with a top-level function
  • Module a invokes a function from b.
  • You want to patch the function in b to test module a in isolation.

File Hierarchy:

mypkg
|- __init__.py    // empty
|- a.py
|- b.py
|- test
  |- __init__.py  // empty
  |- test_a.py

b.py

def b():
  return "World!"

a.py

from .b import b

def a():
  return "Hello " + b()

test_a.py

import unittest
from unittest.mock import patch
from ..a import a

class TestFunctionA(unittest.TestCase):
    def test_a_no_mock(self):
        message = a()
        self.assertEqual(message, "Hello World!")

    @patch('mypkg.a.b')
    def test_mytest(self, mocked_func):
        mocked_func.return_value = "Stack Overflow!"
        message = a()
        self.assertEqual(message, "Hello Stack Overflow!")
        mocked_func.assert_called_once()

Running the tests from the folder containing mypkg

$ ls
mypkg
$ python -m unittest discover
..
----------------------------------------------------------------------
Ran 2 tests in 0.001s

OK

Upvotes: 1

Related Questions