Reputation: 327
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
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.
from .a import a
in your __init__.py
.
a
via from mypkg import a
or import mypkg.a
a
via from mypkg.a import a
or import mypkg.a.a
a
to something else.Upvotes: 2
Reputation: 1736
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
.return_value
should be assigned within your test, not within the decorator.stuff
isn't in the scope of your test class.Here's a working example of the system you describe:
a
and b
, each with a top-level functiona
invokes a function from b
.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