Mocking a path which is given in a method - Python - python

How can I mock the path (".test/locations.yml), because it does not exist in this project where I run my test. It exists in the CI environment.
I test my function get_matches_mr and then it says path location file not found
Do you have any idea?
Code
def read_location_file():
locations_file_path = os.path.join(".test/location.yml")
if not os.path.isfile(locations_file_path):
raise RuntimeError("Location file not found: " + locations_file_path)
with open(locations_file_path, "r") as infile:
location_file = yaml.safe_load(infile.read())
test_locations= location_file["paths"]
return test_locations
def get_matches_mr(self):
merge_request = MergeRequest()
locations = self.read_location_file()
data_locations= merge_request.get_matches(locations)
return data_locations

Like suggested in the comment, I would also say the best way to test such a scenario is to mock read_location_file. Because mocking the file system methods like os.path.join would mean that you limit the test to a certain implementation, which is a bad practice. The unit test suite should not know about the implementation detail, but only about the interfaces to be tested. Usually, in test driven development you write the test before the logic is implemented. This way you would not even know os.path.join is used.
The following code shows how to mock the read_location_file method. Assuming the class containing your two methods is called ClassToBeTested (replace with your actual class name).
import os.path
from class_to_test import ClassToBeTested
def test_function_to_test(tmpdir, monkeypatch):
def mockreturn():
return [
os.path.join(tmpdir, "sample/path/a"),
os.path.join(tmpdir, "sample/path/b"),
os.path.join(tmpdir, "sample/path/c"),
]
monkeypatch.setattr(ClassToBeTested, 'read_location_file', mockreturn)
c = ClassToBeTested()
assert c.get_matches_mr()
Note: I use the fixtures tmpdir and monkeypatch, which are both built-ins of pytest:
See this answer to find some info about tmpdir (in the linked answer I explained tmp_path, but it provides the same concept as tmpdir; the difference is tmp_path returns a pathlib.Path object, and tmpdir returns a py.path.local object).
monkeypatch is a pytest fixture that provides methods for mocking/patching of objects.

Split your function into two parts:
Finding and opening the correct file.
Reading and parsing the opened file.
Your function only does the second part; the call can be responsible for the first part.
def read_location_file(infile):
location_file = yaml.safe_load(infile.read())
test_locations= location_file["paths"]
return test_locations
Your test code can then use something like io.StringIO to verify that your function can parse it correctly.
def test_read_location():
assert read_location_file(io.StringIO("...")) == ...
Your production code will handle opening the file:
with open(location_file_path) as f:
locations = read_location_file(f)

Related

pytest - how to assert if a method of a class is called inside a method

I am trying to figure out how to know if a method of class is being called inside a method.
following is the code for the unit test:
# test_unittes.py file
def test_purge_s3_files(mocker):
args = Args()
mock_s3fs = mocker.patch('s3fs.S3FileSystem')
segment_obj = segments.Segmentation()
segment_obj.purge_s3_files('sample')
mock_s3fs.bulk_delete.assert_called()
inside the purge_s3_file method bulk_delete is called but when asserting it says that the method was expected to be called and it is not called!
mocker = <pytest_mock.plugin.MockerFixture object at 0x7fac28d57208>
def test_purge_s3_files(mocker):
args = Args()
mock_s3fs = mocker.patch('s3fs.S3FileSystem')
segment_obj = segments.Segmentation(environment='qa',
verbose=True,
args=args)
segment_obj.purge_s3_files('sample')
> mock_s3fs.bulk_delete.assert_called()
E AssertionError: Expected 'bulk_delete' to have been called.
I don't know how to test this and how to assert if the method is called!
Below you can find the method being testing:
# segments.py file
import s3fs
def purge_s3_files(self, prefix=None):
bucket = 'sample_bucket'
files = []
fs = s3fs.S3FileSystem()
if fs.exists(f'{bucket}/{prefix}'):
files.extend(fs.ls(f'{bucket}/{prefix}'))
else:
print(f'Directory {bucket}/{prefix} does not exist in s3.')
print(f'Purging S3 files from {bucket}/{prefix}.')
print(*files, sep='\n')
fs.bulk_delete(files)
The problem you are facing is that the mock you are setting up is mocking out the class, and you are not using the instance to use and check your mocks. In short, this should fix your problem (there might be another issue explained further below):
m = mocker.patch('s3fs.S3FileSystem')
mock_s3fs = m.return_value # (or mock_s3())
There might be a second problem in how you are not referencing the right path to what you want to mock.
Depending on what your project root is considered (considering your comment here) your mock would need to be referenced accordingly:
mock('app.segments.s3fs.S3FileSystem')
The rule of thumb is that you always want to mock where you are testing.
If you are able to use your debugger (or output to your console) you will (hopefully :)) see that your expected call count will be inside the return_value of your mock object. Here is a snippet from my debugger using your code:
You will see the call_count attribute set to 1. Pointing back to what I mentioned at the beginning of the answer, by making that change, you will now be able to use the intended mock_s3fs.bulk_delete_assert_called().
Putting it together, your working test with modification runs as expected (note, you should also set up the expected behaviour and assert the other fs methods you are calling in there):
def test_purge_s3_files(mocker):
m = mocker.patch("app.segments.s3fs.S3FileSystem")
mock_s3fs = m.return_value # (or m())
segment_obj = segments.Segmentation(environment='qa',
verbose=True,
args=args)
segment_obj.purge_s3_files('sample')
mock_s3fs.bulk_delete.assert_called()
Python mock testing depends on where the mock is being used. So you have the mock the function calls where it is imported.
Eg.
app/r_executor.py
def r_execute(file):
# do something
But the actual function call happens in another namespace ->
analyse/news.py
from app.r_executor import r_execute
def analyse():
r_execute(file)
To mock this I should use
mocker.patch('analyse.news.r_execute')
# not mocker.patch('app.r_executor.r_execute')

Mock open using context manager and get the argument of open

first of all, sorry if this has been answered before but I have been looking all over the place and could not find anything.
I have a function that does this (in summary):
def func(path):
with open(path) as f:
json.load(f)
I want to unit test it and what I would like is that when I use open as context manager, f is the name of the argument I passed to open. The issue I am having is that I cannot mock the enter method in the proper module, it breaks everytime. This is whay I am doing so far:
def setUp(self):
# Setup the content of the config files for the tests
json.load = MagicMock(side_effect=file_content)
# Opening a file returns the name of the file
builtins.open = MagicMock(side_effect=lambda x: x) #1
builtins.open.__enter__ = MagicMock(side_effect=builtins.open.return_value) #2
the MagicMock number 1 does what I expect it to do, when I run open("test") it returns "test". Nonetheless I cannot mock enter to return whatever I passed to open, it always fails with AttributeError: __enter__. I have tried also doing
builtins.open.return_value.__enter__ = MagicMock...
with no luck. Can anyone think on a way that this can be achieved? I have seen how to mock magic methods and I have thought I was comfortable with unittest mocking but in this case I cannot find a fitting solution. Thanks!
I got it!
I had to define the side_effect dinamically but seems to be working fine. This is what I did:
def setUp(self):
# Setup the content of the config files for the tests
json.load = MagicMock(side_effect=file_content)
# Opening a file returns the name of the file
def get_mock_context(filename):
mock_context = MagicMock()
mock_context.__enter__.return_value = filename
mock_context.__exit__.return_value = False
return mock_context
builtins.open = MagicMock(side_effect=get_mock_context)
In this way, the returned value is a mock that has the __enter__ and __exit__ methods returning exactly the filename I passed to the call of open.

Python: how to create a positive test for procedures?

I have a class with some #staticmethod's that are procedures, thus they do not return anything / their return type is None.
If they fail during their execution, they throw an Exception.
I want to unittest this class, but I am struggling with designing positive tests.
For negative tests this task is easy:
assertRaises(ValueError, my_static_method(*args))
assertRaises(MyCustomException, my_static_method(*args))
...but how do I create positive tests? Should I redesign my procedures to always return True after execution, so that I can use assertTrue on them?
Without seeing the actual code it is hard to guess, however I will make some assumptions:
The logic in the static methods is deterministic.
After doing some calculation on the input value there is a result
and some operation is done with this result.
python3.4 (mock has evolved and moved over the last few versions)
In order to test code one has to check that at least in the end it produces the expected results. If there is no return value then the result is usually stored or send somewhere. In this case we can check that the method that stores or sends the result is called with the expected arguments.
This can be done with the tools available in the mock package that has become part of the unittest package.
e.g. the following static method in my_package/my_module.py:
import uuid
class MyClass:
#staticmethod
def my_procedure(value):
if isinstance(value, str):
prefix = 'string'
else:
prefix = 'other'
with open('/tmp/%s_%s' % (prefix, uuid.uuid4()), 'w') as f:
f.write(value)
In the unit test I will check the following:
open has been called.
The expected file name has been calculated.
openhas been called in write mode.
The write() method of the file handle has been called with the expected argument.
Unittest:
import unittest
from unittest.mock import patch
from my_package.my_module import MyClass
class MyClassTest(unittest.TestCase):
#patch('my_package.my_module.open', create=True)
def test_my_procedure(self, open_mock):
write_mock = open_mock.return_value.write
MyClass.my_procedure('test')
self.assertTrue(open_mock.call_count, 1)
file_name, mode = open_mock.call_args[0]
self.assertTrue(file_name.startswith('/tmp/string_'))
self.assertEqual(mode, 'w')
self.assertTrue(write_mock.called_once_with('test'))
If your methods do something, then I'm sure there should be a logic there. Let's consider this dummy example:
cool = None
def my_static_method(something):
try:
cool = int(something)
except ValueError:
# logs here
for negative test we have:
assertRaises(ValueError, my_static_method(*args))
and for possitive test we can check cool:
assertIsNotNone(cool)
So you're checking if invoking my_static_method affects on cool.

Python unit test that uses an external data file

I have a Python project that I'm working on in Eclipse and I have the following file structure:
/Project
/projectname
module1.py
module2.py
# etc.
/test
testModule1.py
# etc.
testdata.csv
In one of my tests I create an instance of one of my classes giving 'testdata.csv' as a parameter. This object does open('testdata.csv') and reads the contents.
If I run just this single test file with unittest everything works and the file is found and read properly. However if I try to run all my unit tests (i.e. run by right clicking the test directory rather than the individual test file), I get an error that file could not be found.
Is there any way to get around this (other than providing an absolute path, which I'd prefer not to do)?
Usually what I do is define
THIS_DIR = os.path.dirname(os.path.abspath(__file__))
at the top of each test module. Then it doesn't matter what working directory you're in - the file path is always the same relative to the where the test module sits.
Then I use something like this is in my test (or test setup):
my_data_path = os.path.join(THIS_DIR, os.pardir, 'data_folder/data.csv')
Or in your case, since the data source is in the test directory:
my_data_path = os.path.join(THIS_DIR, 'testdata.csv')
Edit: for modern python
from pathlib import Path
THIS_DIR = Path(__file__).parent
my_data_path = THIS_DIR.parent / 'data_folder/data.csv'
# or if it's in the same directory
my_data_path = THIS_DIR / 'testdata.csv'
Unit test that access the file system are generally not a good idea. This is because the test should be self contained, by making your test data external to the test it's no longer immediately obvious which test the csv file belongs to or even if it's still in use.
A preferable solution is to patch open and make it return a file-like object.
from unittest import TestCase
from unittest.mock import patch, mock_open
from textwrap import dedent
class OpenTest(TestCase):
DATA = dedent("""
a,b,c
x,y,z
""").strip()
#patch("builtins.open", mock_open(read_data=DATA))
def test_open(self):
# Due to how the patching is done, any module accessing `open' for the
# duration of this test get access to a mock instead (not just the test
# module).
with open("filename", "r") as f:
result = f.read()
open.assert_called_once_with("filename", "r")
self.assertEqual(self.DATA, result)
self.assertEqual("a,b,c\nx,y,z", result)
In my opinion the best way to handle these cases is to program via inversion of control.
In the two sections below I primarily show how a no-inversion-of-control solution would look like. The second section shows a solution with inversion of control and how this code can be tested without a mocking-framework.
In the end I state some personal pros and cons that do not at all have the intend to be correct and or complete. Feel free to comment for augmentation and correction.
No inversion of control (no dependency injection)
You have a class that uses the std open method from python.
class UsesOpen(object):
def some_method(self, path):
with open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen()
uses_open.some_method('/my/path')
Here I have used open explicitly in my code, so the only way to write tests for it would be to use explicit test-data (files) or use a mocking-framework like Dunes suggests.
But there is still another way:
My suggestion: Inversion of control (with dependency injection)
Now I rewrote the class differently:
class UsesOpen(object):
def __init__(self, myopen):
self.__open = myopen
def some_method(self, path):
with self.__open(path) as f:
process(f)
# how the class is being used in the open
def main():
uses_open = UsesOpen(open)
uses_open.some_method('/my/path')
In this second example I injected the dependency for open into the constructor (Constructor Dependency Injection).
Writing tests for inversion of control
Now I can easily write tests and use my test version of open when I need it:
EXAMPLE_CONTENT = """my file content
as an example
this can be anything"""
TEST_FILES = {
'/my/long/fake/path/to/a/file.conf': EXAMPLE_CONTENT
}
class MockFile(object):
def __init__(self, content):
self.__content = content
def read(self):
return self.__content
def __enter__(self):
return self
def __exit__(self, type, value, tb):
pass
class MockFileOpener(object):
def __init__(self, test_files):
self.__test_files = test_files
def open(self, path, *args, **kwargs):
return MockFile(self.__test_files[path])
class TestUsesOpen(object):
def test_some_method(self):
test_opener = MockFileOpener(TEST_FILES)
uses_open = UsesOpen(test_opener.open)
# assert that uses_open.some_method('/my/long/fake/path/to/a/file.conf')
# does the right thing
Pro/Con
Pro Dependency Injection
no need to learn mocking framework for tests
complete control over the classes and methods that have to be faked
also changing and evolving your code is easier in general
code quality normally improves, as one of the most important
factors is being able to respond to changes as easy as possible
using dependency injection and a dependency injection framework
is generally a respected way to work on a project https://en.wikipedia.org/wiki/Dependency_injection
Con Dependency Injection
a little bit more code to write in general
in tests not as short as patching a class via #patch
constructors can get overloaded with dependencies
you need to somehow learn to use dependency-injection
For test discovery it is recommended to make your test folder a package. In this case you can access resources in the test folder using importlib.resources (mind Python version compatibility of the individual functions, there are backports available as importlib_resources), as described here, e.g. like:
import importlib.resources
test_file_path_str = str(importlib.resources.files('tests').joinpath('testdata.csv'))
test_function_expecting_filename(test_file_path_str)
Like this you do not need to rely on inferring file locations of your code.
Your tests should not open the file directly, every test should copy the file and work with its copy.

Python *apropos* command

I am looking for all the functions that have a parameter called adjustable.
One of these function is matplotlib.pyplot.figure ().add_axes. help(matplotlib.pyplot.figure ().add_axes) describes that parameter, that can be present in kwargs dictionary.
I tried pydoc.apropos
from pydoc import apropos
In [8]: apropos ('adjustable')
No handlers could be found for logger "OpenGL.Tk"
In [9]:
This is all what it returned apropos(key).
One brute force way to find what I am looking for is to grep the source code of python from /usr/share/. But I need to do it from the current python environment (only what is present loaded in evaluator).
I just noticed you're using IPython, if so, there's an extension called grasp which implements its own version of apropos that may be useful here.
The documentation even uses matplotlib in its example.
apropos only searches the one-line summaries of all modules, not each docstring of each single function, as that wouldn't be feasable (already importing every single module as apropos does and searching it's docstring is quite some overhead).
And even if it would, in the case of add_axes it wouldn't find anything because your search term isn't part of the one-line description.
I'm affraid if you need such functionality, you'd need to implement it yourself.
Here is a quickly written function that walks all current packages in the specified path, using ast to find matching parameters, and returning (filename, funcname, line_no) for each match.
import ast
import pkgutil
import os.path
class FindParameter(ast.NodeVisitor):
def __init__(self, parameter):
self.parameter = parameter
self.found = []
def visit_FunctionDef(self, node):
for arg in node.args.args:
if getattr(arg, 'id', None) == self.parameter:
self.found.append(node)
def apropos(parameter, path=None):
paramFinder = FindParameter(parameter)
for importer, modname, is_package in pkgutil.iter_modules(path=path):
try:
loader = importer.find_module(modname)
loader.get_code()
if loader.source:
tree = ast.parse(loader.source, filename=loader.filename)
paramFinder.visit(tree)
except SyntaxError:
pass # avoid any broken code in the current path
return [ (loader.filename, found.name, found.lineno) for found in paramFinder.found ]
all_selfs = apropos('self')

Categories