How to mock readlines() in Python unit tests - python-3.x

I am trying to write a unit test to a class init that reads from a file using readlines:
class Foo:
def __init__(self, filename):
with open(filename, "r") as fp:
self.data = fp.readlines()
with sanity checks etc. included.
Now I am trying to create a mock object that would allow me to test what happens here.
I try something like this:
TEST_DATA = "foo\nbar\nxyzzy\n"
with patch("my.data.class.open", mock_open(read_data=TEST_DATA), create=True)
f = Foo("somefilename")
self.assertEqual(.....)
The problem is, when I peek into f.data, there is only one element:
["foo\nbar\nxyzzy\n"]
Which means whatever happened, did not get split into lines but was treated as one line. How do I force linefeeds to happen in the mock data?

This will not work with a class name
with patch("mymodule.class_name.open",
But this will work by mocking the builtin directly, builtins.open for python3
#mock.patch("__builtin__.open", new_callable=mock.mock_open, read_data=TEST_DATA)
def test_open3(self, mock_open):
...
or this without class by mocking the module method
def test_open(self):
with patch("mymodule.open", mock.mock_open(read_data=TEST_DATA), create=True):
...

#Gang's answer pointed me to the right direction but it's not a complete working solution. I have added few details here which makes it a working code without any tinkering.
# file_read.py
def read_from_file():
# Do other things here
filename = "file_with_data"
with open(filename, "r") as f:
l = f.readline()
return l
# test_file_read.py
from file_read import read_from_file
from unittest import mock
import builtins
##mock.patch.object(builtins, "open", new_callable=mock.mock_open, read_data="blah")
def test_file_read(mock_file_open):
output = read_from_file()
expected_output = "blah"
assert output == expected_output

Related

How to assert contents written to io.BytesIO in unit tests

Assume the following minimal code:
def upload(fileobj, key):
# e.g. S3.Bucket.upload_fileobj()
pass
def work():
with io.BytesIO() as tmp:
tmp.write(b'abc') # here can be a tarfile created
upload(tmp, "file")
I would like to assert the contents of tmp in unit tests. How to do it correctly?
This way does not work:
from unittest.mock import MagicMock
upload = MagicMock()
work()
with upload.mock_calls[0][1][0] as f:
f.readlines() # ValueError: I/O operation on closed file.
I've figured out one solution by myself (thanks to Tim Roberts inspirations in comments), which is using side_effect option (of unittest.mock.MagicMock constructor).
import unittest.mock
eavesdropped = []
def upload_mock(fileobj, key):
fileobj.seek(0)
eavesdropped.append(fileobj.read())
with unittest.mock.patch('__main__.upload', side_effect=upload_mock) as upl:
work()
assert eavesdropped[0] == b'abc'
work()
assert eavesdropped[1] == b'abc'

How can I redirect hardcoded calls to open to custom files?

I've written some python code that needs to read a config file at /etc/myapp/config.conf . I want to write a unit test for what happens if that file isn't there, or contains bad values, the usual stuff. Lets say it looks like this...
""" myapp.py
"""
def readconf()
""" Returns string of values read from file
"""
s = ''
with open('/etc/myapp/config.conf', 'r') as f:
s = f.read()
return s
And then I have other code that parses s for its values.
Can I, through some magic Python functionality, make any calls that readconf makes to open redirect to custom locations that I set as part of my test environment?
Example would be:
main.py
def _open_file(path):
with open(path, 'r') as f:
return f.read()
def foo():
return _open_file("/sys/conf")
test.py
from unittest.mock import patch
from main import foo
def test_when_file_not_found():
with patch('main._open_file') as mopen_file:
# Setup mock to raise the error u want
mopen_file.side_effect = FileNotFoundError()
# Run actual function
result = foo()
# Assert if result is expected
assert result == "Sorry, missing file"
Instead of hard-coding the config file, you can externalize it or parameterize it. There are 2 ways to do it:
Environment variables: Use a $CONFIG environment variable that contains the location of the config file. You can run the test with an environment variable that can be set using os.environ['CONFIG'].
CLI params: Initialize the module with commandline params. For tests, you can set sys.argv and let the config property be set by that.
In order to mock just calls to open in your function, while not replacing the call with a helper function, as in Nf4r's answer, you can use a custom patch context manager:
from contextlib import contextmanager
from types import CodeType
#contextmanager
def patch_call(func, call, replacement):
fn_code = func.__code__
try:
func.__code__ = CodeType(
fn_code.co_argcount,
fn_code.co_kwonlyargcount,
fn_code.co_nlocals,
fn_code.co_stacksize,
fn_code.co_flags,
fn_code.co_code,
fn_code.co_consts,
tuple(
replacement if call == name else name
for name in fn_code.co_names
),
fn_code.co_varnames,
fn_code.co_filename,
fn_code.co_name,
fn_code.co_firstlineno,
fn_code.co_lnotab,
fn_code.co_freevars,
fn_code.co_cellvars,
)
yield
finally:
func.__code__ = fn_code
Now you can patch your function:
def patched_open(*args):
raise FileNotFoundError
with patch_call(readconf, "open", "patched_open"):
...
You can use mock to patch a module's instance of the 'open' built-in to redirect to a custom function.
""" myapp.py
"""
def readconf():
s = ''
with open('./config.conf', 'r') as f:
s = f.read()
return s
""" test_myapp.py
"""
import unittest
from unittest import mock
import myapp
def my_open(path, mode):
return open('asdf', mode)
class TestSystem(unittest.TestCase):
#mock.patch('myapp.open', my_open)
def test_config_not_found(self):
try:
result = myapp.readconf()
assert(False)
except FileNotFoundError as e:
assert(True)
if __name__ == '__main__':
unittest.main()
You could also do it with a lambda like this, if you wanted to avoid declaring another function.
#mock.patch('myapp.open', lambda path, mode: open('asdf', mode))
def test_config_not_found(self):
...

How to initialize python watchdog pattern matching event handler

I'm using the Python Watchdog to monitor a directory for new files being created. Several different types of files are created in said directory but I only need to monitor a single file type, hence I use the Watchdog PatternMatchingEventHandler, where I specify the pattern to monitor using the patterns keyword.
To correctly execute the code under the hood (not displayed here) I need to initialize an empty dataframe in my event-handler, and I am having trouble getting this to work. If I remove the __init__ in the code below, everything works just fine btw.
I used the code in this answer as inspiration for my own.
The code I have set up looks as follows:
from watchdog.observers import Observer
from watchdog.events import PatternMatchingEventHandler
import time
import pandas as pd
import numpy as np
from multiprocessing import Pool
class HandlerEQ54(PatternMatchingEventHandler):
def __init__(self):
#Initializing an empty dataframe for storage purposes.
data_54 = pd.DataFrame(columns = ['Barcode','DUT','Step12','Step11','Np1','Np2','TimestampEQ54'])
#Converting to INT for later purposes
data_54[['Barcode','DUT']]=data_54[['Barcode','DUT']].astype(np.int64)
self.data = data_54
def on_created(self,event):
if event.is_directory:
return True
elif event.event_type == 'created':
#Take action here when a file is created.
print('Found new files:')
print(event.src_path)
time.sleep(0.1)
#Creating process pool to return data
pool1 = Pool(processes=4)
#Pass file to parsing function and return parsed result.
result_54 = pool1.starmap(parse_eq54,[(event.src_path,self.data)])
#returns the dataframe rather than the list of dataframes returned by starmap
self.data = result_54[0]
print('Data read: ')
print(self.data)
def monitorEquipment(equipment):
'''Uses the Watchdog package to monitor the data directory for new files.
See the HandlerEQ54 and HandlerEQ51 classes in multiprocessing_handlers for actual monitoring code. Monitors each equipment.'''
print('equipment')
if equipment.upper() == 'EQ54':
event_handler = HandlerEQ54(patterns=["*.log"])
filepath = '/path/to/first/file/source/'
# set up observer
observer = Observer()
observer.schedule(event_handler, path=filepath, recursive=True)
observer.daemon=True
observer.start()
print('Observer started')
# monitor
try:
while True:
time.sleep(5)
except KeyboardInterrupt:
observer.unschedule_all()
observer.stop()
observer.join()
However, when I execute monitorEquipment I receive the following error message:
TypeError: __init__() got an unexpected keyword argument 'patterns'
Evidently I'm doing something wrong when I'm initializing my handler class, but I'm drawing a blank as to what that is (which probably reflects my less-than-optimal understanding of classes). Can someone advice me on how to correctly initialize the empty dataframe in my HandlerEQ54 class, to not get the error I do?
Looks like you are missing the patterns argument from your __init__ method, you'll also need a super() call to the __init__ method of the parent class (PatternMatchingEventHandler), so you can pass the patterns argument upwards.
it should look something like this:
class HandlerEQ54(PatternMatchingEventHandler):
def __init__(self, patterns=None):
super(HandlerEQ54, self).__init__(patterns=patterns)
...
event_handler = HandlerEQ54(patterns=["*.log"])
or, for a more generic case and to support all of PatternMatchingEventHandler's arguments:
class HandlerEQ54(PatternMatchingEventHandler):
def __init__(self, *args, **kwargs):
super(HandlerEQ54, self).__init__(*args, **kwargs)
...
event_handler = HandlerEQ54(patterns=["*.log"])

Python - Mock class init that instantiates another class inside

I have the following python file board.py:
def __init__(self, language):
self.foo = Foo(language)
self.words = Aux(self.foo)
And I'm creating this test_file:
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.board = Board('pt')
def test_total_time(self):
self.board.total_time(True)
#some assert
But I'm getting a FileNotFoundError because Aux.___init____() calls a self.foo.method() that opens a file and reads from it.
Is there a way to mock self.foo.method(), or the class Aux?
You will want to patch the module. If you give me the name of the test file and the class you are testing. I can finish this answer for you.
In the test file:
import unittest
def BoardTestCase(unittest.TestCase):
#classmethod
def setUpClass(cls):
super().setUpClass()
cls.aux_mock = unittest.mock.patch('file_undertest.Aux')
cls.board = Board('pt')
def test_total_time(self):
self.board.total_time(True)
#some assert
I would suggest using pytest instead of the standard library unittest. Your tests will be written as functions meaning you can reuse the Board class only when needed to. You can set more robust fixtures (Board class test cases) and the mocker extension is more intuitive if you spend the 15 minutes to wrap your head around it.

mock `readlines()` in python unit tests

I am trying to mock a readlines() object in python unit tests. What I have so far is
class Sample:
def read_file(filename):
with open(filename, "r") as f:
lines = f.readlines()
I want to write a unit test for readlines() object.
So far, I have come up with the following.
TEST_DATA = "test\ntest2\n"
#mock.patch("builtins.open")
def test_open(mock_open):
mock_open.side_effect = [mock_open(read_data=TEST_DATA).return_value]
assert mock_open.side_effect == Sample.read_file()
My question here is, how do I assert the returned value of mock_open is the same as the returned value of the actual read_file function in the Sample class? This is where I am failing and not able to go any further. Any help on this is much appreciated! Thank you in advance!
In unittest.mock docs there is an example that may help you
Here is the docs example adapted to your code.
from unittest.mock import patch
class Sample:
def read_file(filename):
with open(filename, "r") as f:
lines = f.readlines()
return lines
TEST_DATA = "test\ntest2\n"
def test_open(mock_open):
with patch('__main__.open', mock_open(read_data=TEST_DATA)) as m:
s = Sample()
res = s.read_file('foo')
assert res == TEST_DATA

Resources