Python Pytest mocking three functions - python-3.x

So I am quite new to mocking. I think I need to mock two functions.
Function under test
def get_text_from_pdf(next_pdfs_path):
# TODO test this function
"""
pulls all text from a PDF document and returns as a string.
Parameters:
next_pdfs_path (str): file path use r"" around path.
Returns:
text (str): string of text
"""
if os.path.isfile(next_pdfs_path): # check file is a real file/filepath
try:
text = ''
with fitz.open(next_pdfs_path) as doc: # using PyMuPDF
for page in doc:
text += page.getText()
return text
except (RuntimeError, IOError):
pass
pass
test code first try
from unittest import mock
#mock.patch("content_production.fitz.open", return_value='fake_file.csv', autospec=True)
def test_get_text_from_pdf(mock_fitz_open):
assert cp.get_text_from_pdf('fake_file.csv') == 'fake_file.csv'
error
E AssertionError: assert None == 'fake_file.csv'
E + where None = <function get_text_from_pdf at 0x00000245EDF8CAF0>('fake_file.csv')
E + where <function get_text_from_pdf at 0x00000245EDF8CAF0> = cp.get_text_from_pdf
Do I need to mock both fitz.open and os.path.isfile? How could that be done if yes?
EDIT
Following njriasan feedback I have tried this
#mock.patch("content_production.os.path.isfile", return_value=True, autospec=True)
#mock.patch("content_production.fitz.Page.getText")
#mock.patch("content_production.fitz.open")
def test_get_text_from_pdf(mock_fitz_open, mock_path_isfile, mock_gettext):
mock_fitz_open.return_value.__enter__.return_value = 'test'
assert cp.get_text_from_pdf('test') == 'test'
But now getting this error.
> text += page.getText()
E AttributeError: 'str' object has no attribute 'getText'

I think there are a couple issues with what you are doing. The first problem I see is that I think you are mocking the wrong function. By mocking fitz.open(next_pdfs_path) you are still expecting:
for page in doc:
text += page.getText()
to execute properly. I'd suggest that you wrap this entire with statement and text result updating in a helper function and then mock that. If the file path doesn't actually exist on your system then you will also need to mock os.path.isfile. I believe that can be done by adding a second decorator (I don't think there is any limit).

Related

Pytest mock using decorator to mock function containing context manager return value not passed

So I am struggling with mocking this function due to the context manager.
Function under test
import fitz
def with_test_func(next_pdfs_path):
text = ''
with fitz.open(next_pdfs_path) as doc:
text = doc
return text
Test code
#mock.patch("content_production.fitz.open.__enter__", return_value='value_out')
def test_with_test_func(mock_fitz_open):
assert cp.with_test_func('value_in') == 'value_out'
Error
RuntimeError: cannot open value_in: No such file or directory
I have tested this without the context manager and it works. So how would I fix this? thanks
Edit
So as suggested by #MrBean I tried this
#mock.patch("content_production.fitz.open.return_value.__enter__", return_value='value_out')
def test_with_test_func(mock_fitz_open):
assert cp.with_test_func('value_in') == 'value_out'
It gives me this error
thing = <class 'fitz.fitz.Document'>, comp = 'return_value', import_path = 'content_production.fitz.open.return_value'
def _dot_lookup(thing, comp, import_path):
try:
return getattr(thing, comp)
except AttributeError:
> __import__(import_path)
E ModuleNotFoundError: No module named 'content_production.fitz'; 'content_production' is not a package
The problem is that return_value is a property of the mock, not of the patched function, so you cannot put it into the patch argument string. Instead, you have to set the return value on the mock for the open method:
#mock.patch("content_production.fitz.open")
def test_with_test_func(mock_fitz_open):
mock_fitz_open.return_value.__enter__.return_value = 'value_out'
assert cp.with_test_func('value_in') == 'value_out'

Correct way to mock decorated method in Python

I have the class with .upload method. This method is wrapped using the decorator:
#retry(tries=3)
def upload(self, xml_file: BytesIO):
self.client.upload(self._compress(xml_file))
I need to test if it runs 3 times if some exception occurs.
My test looks like:
#mock.patch("api.exporter.jumbo_uploader.JumboZipUploader.upload")
def test_upload_xml_fail(self, mock_upload):
"""Check if decorator called the compress function 3 times"""
generator = BrandBankXMLGenerator()
file = generator.generate()
uploader = JumboZipUploader()
uploader.upload = retry(mock_upload)
mock_upload.side_effect = Exception("Any exception")
uploader.upload(file)
self.assertEqual(mock_upload.call_count, 3)
I have read that the default behavior of python decorators assumes that the function inside the test will be unwrapped and I need to wrap it manually.
I did that trick, but the code fails with AssertionError: 0 != 3.
So, what is the right way here to wrap the decorated method properly?

Calling various function based on input in python 3.x

I'm writing some code to get various data from a class (which extracts data from a '.csv' file). I was wondering if there was a way to call one of these methods based off the name of an input
I've attempted to create a function called get(), which takes in 'param_name' - the name of the method contained within the class that I want to call. I was wondering if there was a more elegant way to solve this without creating a large amount of if statements.
def get(param_name):
# Some initialisation of the .csv file goes here. This works as intended.
list_of_objects = [] # Initialised above, as a list of objects with methods function1(), function2() for getting data out of the .csv
for item in list_of_objects:
if param_name == "name of function 1":
return function1()
if param_name == "name of function 2":
return function2()
You could store your functions ina a dictionary as such:
function_dict = {
'function_1': function_1,
'function_2': function_2
}
To use these you could do:
function_to_use = function_dict.get(param_name)
function_to_use(*args, **kwargs) # *args, **kwargs are arguments to be used.
If you want to return a list after you have applied the function to all item in list_of_objects instead of the for loop you could do:
list(map(function_to_use, list_of_objects))
You could use __getattribute__:
class Alpha:
def f1(self):
print("F1")
x = Alpha()
x.__getattribute__('f1')()
You can do that using globals(), globals() returns a dict containing all methods and attributes.
def fun1():
print('this is fun1')
def fun2():
print('this is fun2')
def get(func_name):
globals()[func_name]()
get('fun1')
get('fun2')
Will Output:
this is fun1
this is fun2

import and rename functions from a folder - Python 3 [duplicate]

I would like to import all methods from a module with altered names.
For instance, instead of
from module import repetitive_methodA as methodA, \
repetitive_Class1 as Class1, \
repetitive_instance4 as instance4
I'd prefer something along the lines of
from module import * as *-without-"repetitive_"
this is a rephrasing of this clumsy unanswered question, I have not been able to find a solution or similar questions yet.
You can do it this way:
import module
import inspect
for (k,v) in inspect.getmembers(module):
if k.startswith('repetitive_'):
globals()[k.partition("_")[2]] = v
Edit in response to the comment "how is this answer intended to be used?"
Suppose module looks like this:
# module
def repetitive_A():
print ("This is repetitive_A")
def repetitive_B():
print ("This is repetitive_B")
Then after running the rename loop, this code:
A()
B()
produces this output:
This is repetitive_A
This is repetitive_B
What I would do, creating a work-around...
Including you have a file named some_file.py in the current directory, which is composed of...
# some_file.py
def rep_a():
return 1
def rep_b():
return 2
def rep_c():
return 3
When you import something, you create an object on which you call methods. These methods are the classes, variables, functions of your file.
In order to get what you want, I thought It'd be a great idea to just add a new object, containing the original functions you wanted to rename. The function redirect_function() takes an object as first parameter, and will iterate through the methods (in short, which are the functions of your file) of this object : it will, then, create another object which will contain the pointer of the function you wanted to rename at first.
tl;dr : this function will create another object which contains the original function, but the original name of the function will also remain.
See example below. :)
def redirect_function(file_import, suffixe = 'rep_'):
# Lists your functions and method of your file import.
objects = dir(file_import)
for index in range(len(objects)):
# If it begins with the suffixe, create another object that contains our original function.
if objects[index][0:len(suffixe)] == suffixe:
func = eval("file_import.{}".format(objects[index]))
setattr(file_import, objects[index][len(suffixe):], func)
if __name__ == '__main__':
import some_file
redirect_function(some_file)
print some_file.rep_a(), some_file.rep_b(), some_file.rep_c()
print some_file.a(), some_file.b(), some_file.c()
This outputs...
1 2 3
1 2 3

How to mock a BulkWriteException in python?

I need to get the information contained in the exception. This is the code I use.
try:
result = yield user_collection.insert_many(content, ordered=False)
except BulkWriteError as e:
print (e)
And in my test when I get into the except with this line,
self.insert_mock.side_effect = [BulkWriteError('')]
it returns me
batch op errors occurred
instead of a MagicMock or a Mock.
How can I mock the BulkWriteError and give it a default return_value and see it when I use print(e)?
Something like this should allow you to test your print was called correctly.
import builtins # mockout print
class BulkWriteErrorStub(BulkWriteError):
''' Stub out the exception so you can bypass the constructor. '''
def __str__:
return 'fake_error'
#mock.patch.object('builtins', 'print')
def testRaisesBulkWrite(self, mock_print):
...
self.insert_mock.side_effect = [BuilkWriteErrorStub]
with self.assertRaises(...):
mock_print.assert_called_once_with('fake_error')
I haven't tested this so feel free to edit it if I made a mistake.

Resources