Which module to patch - python

I have the following directory
/root
/app
/api
my_api.py
/service
my_service.py
/tests
test_api.py
my_api.py
import app
def run_service():
app.service.my_service.service_function()
test_api.py
#patch('app.service.my_service.service_function')
test_run_service(self,mock_service):
mock_service.return_value = 'Mock'
response = self.client.get(url_for('api.run_service')
self.assertTrue(response == expected_responce)
The above works. What I cant figure out, is which module I need to patch, in case I wanted to import service_function in my_apy.py like this:
from app.service.my_service import service_function
If I do the import like above, mock stops working.

You need to patch out app.api.my_api.service_function, since that is the global name already bound to the imported object:
#patch('app.api.my_api.service_function')
test_run_service(self, mock_service):
mock_service.return_value = 'Mock'
response = self.client.get(url_for('api.run_service')
self.assertTrue(response == expected_responce)
See the Where to patch section:
The basic principle is that you patch where an object is looked up, which is not necessarily the same place as where it is defined.

Related

How to reference local import when patching - proper python testing structure

I have a Starlette app and I'm attempting to perform end to end testing on the entire application. A function defined in a.py is called by a function in b.py through a local import, and for testing purpose I would like to replace the function with a self defined function when testing.
Like others, I'm running into issues getting the path string to work. After looking at existing questions on stackoverflow, I think I'm supposed to be patching the reference in b.py, but run into the following error AttributeError: <function b at 0x7f5298087af0> does not have the attribute 'a'
Here's the relevant structure and code
Project_folder
- app
- lib
+ __init__.py
+ a.py
+ b.py
- handle
+ __init__.py
- test
+ test.py
+ main.py
#a.py
def a(id):
return stuff
#b.py
from .a import a
def b():
return a(some_stuff)
#lib.__init__py
from .a import a
from .b import b
routes refers to an async function which calls b somewhere in the code
#main.py
from starlette.applications import Starlette
from handle import routes
app = Starlette(routes=routes)
My attempt at test.py
from starlette.testclient import TestClient
from main import app
import pytest
from mock import patch
client = TestClient(app)
def mock_a(id):
return 'some value'
#patch('app.lib.b.a', new=mock_a)
def test_app(request):
response = client.post('/route', request)
assert response.status_code == 200
I'm very new to mocks and patching, and would appreciate any advice on how I should be setting this up
You import "a" and "b" as functions in init.py. So patch libs, not libs.a or libs.b. if you want to patch inside the module itself the take the imports out of init.py and the you can do something like this:
import libs
libs.b.b = libs.a.a

How to patch a module that hasn't been imported by parent package's __init__.py

I'm trying to test a tool I'm building which uses some jMetalPy functionality. I had/have a previous version working but I am now trying to refactor out some external dependencies (such as the aforementioned jMetalPy).
Project Code & Structure
Here is a minimalist structure of my project.
MyToolDirectory
¦--/MyTool
¦----/__init__.py
¦----/_jmetal
¦------/__init__.py
¦------/core
¦--------/quality_indicator.py
¦----/core
¦------/__init__.py
¦------/run_manager.py
¦----/tests
¦------/__init__.py
¦------/test_run_manager.py
The _jmetal directory is to remove external dependency on the jMetalPy package - and I have copied only the necessary packages/modules that I need.
Minimal contents of run_manager.py
# MyTool\core\run_manager.py
import jmetal
# from jmetal.core.quality_indicators import HyperVolume # old working version
class RunManager:
def __init__(self):
pass
#staticmethod
def calculate_hypervolume(front, ref_point):
if front is None or len(front) < 1:
return 0.
hv = jmetal.core.quality_indicator.HyperVolume(ref_point)
# hv = HyperVolume(ref_point)
hypervolume = hv.compute(front)
return hypervolume
Minimal contents of test_run_manager.py
# MyTool\tests\test_run_manager.py
import unittest
from unittest.mock import MagicMock, Mock, patch
from MyTool import core
class RunManagerTest(unittest.TestCase):
def setUp(self):
self.rm = core.RunManager()
def test_calculate_hypervolume(self):
ref_points = [0.0, 57.5]
front = [None, None]
# with patch('MyTool.core.run_manager.HyperVolume') as mock_HV: # old working version
with patch('MyTool.core.run_manager.jmetal.core.quality_indicator.HyperVolume') as mock_HV:
mock_HV.return_value = MagicMock()
res = self.rm.calculate_hypervolume(front, ref_points)
mock_HV.assert_called_with(ref_points)
mock_HV().compute.assert_called_with(front)
Main Question
When I run a test with the code as-is, I get this error message:
E ModuleNotFoundError: No module named 'MyTool.core.run_manager.jmetal'; 'MyTool.core.run_manager' is not a package
But when I change it to:
with patch('MyTool.core.run_manager.jmetal.core') as mock_core:
mock_HV = mock_core.quality_indicator.HyperVolume
mock_HV.return_value = MagicMock()
res = self.rm.calculate_hypervolume(front, ref_points)
mock_HV.assert_called_with(ref_points)
mock_HV().compute.assert_called_with(front)
... now the test passes. What gives?!
Why can't (or rather, how can) I surgically patch the exact class I want (i.e., HyperVolume) without patching out an entire sub-package as well? Is there a way around this? There may be code in jmetal.core that needs to run normally.
Is the reason this isn't working only because there is no from . import quality_indicator statement in jMetalPy's jmetal\core\__init__.py ?
Because even with patch('MyTool.core.run_manager.jmetal.core.quality_indicator) throws:
E AttributeError: <module 'jmetal.core' from 'path\\to\\venv\\lib\\site-packages\\jmetal\\core\\__init__.py'> does not have the attribute 'quality_indicator'
Or is there something I'm doing wrong?
In the case that it is just about adding those import statements, I could do that in my _jmetal sub-package, but I was hoping to let the user default to their own jMetalPy installation if they already had one by adding this to MyTool\__init__.py:
try:
import jmetal
except ModuleNotFoundError:
from . import _jmetal as jmetal
and then replacing all instances of import jmetal with from MyTool import jmetal. However, I'd run into the same problem all over again.
I feel that there is some core concept I am not grasping. Thanks for the help.

Pytest mocker patch - how to troubleshoot?

I am having what I believe to be a common issue in using mock patching in that I can not figure out the right thing to patch.
I have two questions that I am hoping for help with.
Thoughts on how to fix the specific issue in the below example
And possibly most-importantly pro-tips/pointers/thoughts/suggestions on how to best troubleshoot the "which thing do I patch" question. The problem I'm having is, without a full understanding of how patching works, I really dont even know what I should be looking for and find myself playing a guessing game.
An example using pyarrow that is currently causing me pain:
mymodule.py
import pyarrow
class HdfsSearch:
def __init__(self):
self.fs = self._connect()
def _connect(self) -> object:
return pyarrow.hdfs.connect(driver="libhdfs")
def search(self, path: str):
return self.fs.ls(path=path)
test_module.py
import pyarrow
import pytest
from mymodule import HdfsSearch
#pytest.fixture()
def hdfs_connection_fixture(mocker):
mocker.patch("pyarrow.hdfs.connect")
yield HdfsSearch()
def test_hdfs_connection(hdfs_connection_fixture):
pyarrow.hdfs.connect.assert_called_once() # <-- succeeds
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once() # <-- fails
pytest output:
$ python -m pytest --verbose test_module.py
=========================================================================================================== test session starts ============================================================================================================
platform linux -- Python 3.7.4, pytest-5.0.1, py-1.8.0, pluggy-0.12.0 -- /home/bbaur/miniconda3/envs/dev/bin/python
cachedir: .pytest_cache
rootdir: /home/user1/work/app
plugins: cov-2.7.1, mock-1.10.4
collected 2 items
test_module.py::test_hdfs_connection PASSED [ 50%]
test_module.py::test_hdfs_search FAILED [100%]
================================================================================================================= FAILURES =================================================================================================================
_____________________________________________________________________________________________________________ test_hdfs_search _____________________________________________________________________________________________________________
hdfs_connection_fixture = <mymodule.HdfsSearch object at 0x7fdb4ec2a610>
def test_hdfs_search(hdfs_connection_fixture):
hdfs_connection_fixture.search(".")
> pyarrow.hdfs.HadoopFileSystem.ls.assert_called_once()
E AttributeError: 'function' object has no attribute 'assert_called_once'
test_module.py:16: AttributeError
You're not calling the assert on the Mock object, this is the correct assert:
hdfs_connection_fixture.fs.ls.assert_called_once()
Explanation:
When you access any attribute in a Mock object it will return another Mock object.
Since you patched "pyarrow.hdfs.connect" you've replaced it with a Mock, let's call it Mock A. Your _connect method will return that Mock A and you'll assign it to self.fs.
Now let's break down what's happening in the search method when you call self.fs.ls.
self.fs returns your Mock A object, then the .ls will return a different Mock object, let's call it Mock B. In this Mock B object you're doing the call passing (path=path).
In your assert you're trying to access pyarrow.hdfs.HadoopFileSystem, but it was never patched. You'll need do the assert on the Mock B object, which is at hdfs_connection_fixture.fs.ls
What to Patch
If you change your import in mymodule.py to this from pyarrow.hdfs import connect your patch will stop working.
Why is that?
When you patch something you're changing what a name points to, not the actual object.
Your current patch is patching the name pyarrow.hdfs.connect and in mymodule you're using the same name pyarrow.hdfs.connect so everything is fine.
However, if you use from pyarrow.hdfs import connect mymodule will have imported the real pyarrow.hdfs.connect and created a reference for it with the name mymodule.connect.
So when you call connect inside mymodule you're accessing the name mymodule.connect, which is not patched.
That is why you would need to patch mymodule.connect when using from import.
I'd recommend using from x import y when doing this kind of patching. It makes it more explicit what you're trying to mock and the patch will be limited to that module only, which can prevent unforeseen side-effects.
Source, this section in the Python documentation: Where to patch
To understand how patching works in python let's first understand the import statement.
When we use import pyarrow in a module (mymodule.py in this case) it does two operations :
It searches for the pyarrow module in sys.modules
It binds the results of that search to a name(pyarrow) in the local scope.
By doing something like: pyarrow = sys.modules['pyarrow']
NOTE: import statements in python doesn't execute code. The import statement brings a name into local scope. The execution of code happens as a side-effect only when python can't find a module in sys.modules
So, to patch pyarrow imported in mymodule.py we need to patch the pyarrow name present in the local scope of mymodule.py
patch('mymodule.pyarrow', autospec=True)
test_module.py
import pytest
from mock import Mock, sentinel
from pyarrow import hdfs
from mymodule import HdfsSearch
class TestHdfsSearch(object):
#pytest.fixture(autouse=True, scope='function')
def setup(self, mocker):
self.hdfs_mock = Mock(name='HadoopFileSystem', spec=hdfs.HadoopFileSystem)
self.connect_mock = mocker.patch("mymodule.pyarrow.hdfs.connect", return_value=self.hdfs_mock)
def test_initialize_HdfsSearch_should_connect_pyarrow_hdfs_file_system(self):
HdfsSearch()
self.connect_mock.assert_called_once_with(driver="libhdfs")
def test_initialize_HdfsSearch_should_set_pyarrow_hdfs_as_file_system(self):
hdfs_search = HdfsSearch()
assert self.hdfs_mock == hdfs_search.fs
def test_search_should_retrieve_directory_contents(self):
hdfs_search = HdfsSearch()
self.hdfs_mock.ls.return_value = sentinel.contents
result = hdfs_search.search(".")
self.hdfs_mock.ls.assert_called_once_with(path=".")
assert sentinel.contents == result
Use context managers to patch built-ins
def test_patch_built_ins():
with patch('os.curdir') as curdir_mock: # curdir_mock lives only inside with block. Doesn't lives outside
assert curdir_mock == os.curdir
assert os.curdir == '.'

Mock nested import in Python with MagicMock

My file (ensure_path.py):
import os
def ensure_path(path):
if not os.path.exists(path):
os.makedirs(path)
return path
My test:
import unittest
from unittest.mock import patch, MagicMock
from src.util.fs.ensure_path import ensure_path
FAKE_PATH = '/foo/bar'
class EnsurePathSpec(unittest.TestCase):
#patch('os.path.exists', side_effect=MagicMock(return_value=False))
#patch('os.makedirs', side_effect=MagicMock(return_value=True))
def test_path_exists_false(self, _mock_os_path_exists_false, _mock_os_makedirs):
ensure_path(FAKE_PATH)
_mock_os_path_exists_false.assert_called_with(FAKE_PATH)
_mock_os_makedirs.assert_called_with(FAKE_PATH)
#patch('os.path.exists', side_effect=MagicMock(return_value=True))
#patch('os.makedirs', side_effect=MagicMock(return_value=True))
def test_path_exists_true(self, _mock_os_path_exists_true, _mock_os_makedirs):
ensure_path(FAKE_PATH)
_mock_os_path_exists_true.assert_called_with(FAKE_PATH)
_mock_os_makedirs.assert_not_called()
This is giving the failed assertion Expected call: makedirs('/foo/bar') which I think makes sense because I think I'm mocking os.makedirs at the wrong level.
I've tried replacing #patch('os.makedirs', with #patch('src.util.fs.ensure_path.os.makedirs', and a couple variations of that but I get
ImportError: No module named 'src.util.fs.ensure_path.os'; 'src.util.fs.ensure_path' is not a package
Here is my __init__.py flow :
Is there an obvious fix I'm missing?
Your patch arguments need to be in the reverse order of the #patch decorators.

Mocked patch of os.listdir not working for unittesting

I have a class method that I am trying to test that requires two patched methods, ConfigB.__init__ and listdir:
from os import listdir
from config.ConfigB import ConfigB
class FileRunner(object):
def runProcess(self, cfgA)
cfgB = ConfigB(cfgA)
print(listdir())
I have the following test set up:
import unittest
import unittest.mock imort MagicMock
import mock
from FileRunner import FileRunner
class TestFileRunner(unittest.TestCase):
#mock.patch('ConfigB.ConfigB.__init__')
#mock.patch('os.listdir')
def test_methodscalled(self, osListDir, cfgB):
cfgA = MagicMock()
fileRunner = FileRunner()
cfgB.return_value = None
osListDir.return_value = None
fileRunner.runProcess(cfgA)
Now the patched mock and return value works for ConfigB.ConfigB, but it does not work for os.listdir. When the print(listdir()) method runs I get a list of file in the current directory, not a value of None as I specified in the patched return value. Any idea what is going wrong?
You need to patch your relative path to your code. patch('os.listdir') doesn't works because you need to patch this:
#mock.patch("path.to.your.pythonfile.listdir")
Try with that.

Categories

Resources