unit test of method of class with mocking data in python - python

I have some class SomeClass in file prog.py
class SomeClass:
def __init__(self, file_name):
self.file_name = file_name
self.data_dict = {}
def load(self):
"""
"""
if not os.path.exists(self.file_name):
return False
with open(self.file_name, "r", encoding='iso-8859-1') as file:
skip_lines(4, file)
reader = csv.DictReader(file)
for line in reader:
if line['Region'] == "":#to remove Copiright and disclaimer text at the end
break
label=re.sub('\s+/\s+',"/",line['Region']) #remove spaces around /
if label in self.data_dict.keys():
#if line['Region'] in self.data_dict.keys():
self.data_dict[label][line['Range']] = int(line['All people'])
#self.data_dict[line['Region']][line['Range']] = line['All people']
else:
self.data_dict[label] = {line['Range']:int(line['All people'])}
#self.data_dict[line['Region']] = {line['Range']:line['All people']}
return True
def regions(self):
return list(self.data_dict.keys())
I need to:
Create a file named test_someclass_data.py.
Use import to import the prog module.
In the test_someclass_data.py file, create a unit test class named
TestSomeClass that inherits from unittest.TestCase.
The unit test class should include member functions to test the
regions member function of the SomeClass class.
My code of test_someclass_data.py:
import unittest
import prog
class TestSomeClass(unittest.TestCase):
def test_regions(self):
"""
Test regions function of the SomeClass class.
"""
prog.SomeClass.data_dict ={"key1":"val1","key2":"val2"}
test_output = prog.SomeClass.regions()
expected_output =["key1", "key2"]
self.assertEqual(test_output, expected_output)
I need to test a function of the SomeClass, so I have to create an instance of the class, call method call to store data, then I can compare outputs. Could you advise me, please, how to avoid this and test functions through mocking? Thank you.

Related

How to delete test files when python unittest fails

I'm using python unittest for functions that write data to JSON. I use tearDownClass to delete the output test files so they don't clutter the local repo. Ground truths are also stored as JSON files.
I do want to store the output test files when tests fail, so its easier for troubleshooting.
My current implementation is to use a global boolean keep_file = False. When the unittest fails the assertion, it modifies keep_file = True. tearDownClass only deletes the files when keep_file == False. I don't like the idea of modifying global variables and the try exception blocks for each assert.
import json
import os
import unittest
from src.mymodule import foo1, foo2
# These are defined outside the class on purpose so the classmethods can access them
FILE_1 = "unittest.file1.json"
EXPECTED_FILE_1 = "expected.file1.json"
FILE_2 = "unittest.file2.json"
EXPECTED_FILE_2 = "expected.file2.json"
keep_files = False
class TestRhaPostPayload(unittest.TestCase):
#classmethod
def setUpClass(cls):
cls.get_file1()
cls.get_file2()
#classmethod
def get_file1(cls):
output1 = foo1()
with open(FILE_1, "w") as f:
f.write(output1)
#classmethod
def get_file2(cls):
output2 = foo1()
with open(FILE_2, "w") as f:
f.write(output2)
#classmethod
def tearDownClass(cls):
if not keep_files:
os.remove(FILE_1)
os.remove(FILE_2)
def test_foo1(self):
# code that reads in file1 and expected_file_1
try:
self.assert(expected_output1, output1)
except AssertionError:
global keep_files
keep_files = True
raise
def test_foo2(self):
# code that reads in file2 and expected_file_2
try:
self.assert(expected_output2, output2)
except AssertionError:
global keep_files
keep_files = True
raise
You could simply check, if there were any errors/failures in your test case during tear-down and only delete the files, if there were none.
How to perform this check was explained in this post.
This check is done on a TestCase instance so tearDownClass won't work. But you are using different files in different tests anyway, so you might as well use normal setUp/tearDown to remove the current file.
Here is a working example:
from pathlib import Path
from typing import Optional
from unittest import TestCase
class Test(TestCase):
def all_tests_passed(self) -> bool:
"""Returns `True` if no errors/failures occurred at the time of calling."""
outcome = getattr(self, "_outcome")
if hasattr(outcome, "errors"): # Python <=3.10
result = self.defaultTestResult()
getattr(self, "_feedErrorsToResult")(result, outcome.errors)
else: # Python >=3.11
result = outcome.result
return all(test != self for test, _ in result.errors + result.failures)
def setUp(self) -> None:
super().setUp()
self.test_file: Optional[Path] = None
def tearDown(self) -> None:
super().tearDown()
if self.test_file and self.all_tests_passed():
self.test_file.unlink()
def test_foo(self) -> None:
self.test_file = Path("foo.txt")
self.test_file.touch()
self.assertTrue(True)
def test_bar(self) -> None:
self.test_file = Path("bar.txt")
self.test_file.touch()
self.assertTrue(False)
Running this test case leaves bar.txt in the current working directory, whereas foo.txt is gone.

How to mock imwrite in a loop

I am trying to use Python unittest library to test the following code.
from cv2 import imwrite
import h5py
import lumpy as np
class Myclass:
def __init__(self):
self.data = []
def get_data(self):
return self.data
def load_data(self, path):
count = 10
for i in range(count):
mat_file = path + f'{i}.mat'
with h5py.File(mat_file, 'r') as fin:
data = np.array(fin['data'])
for j in range(len(data)):
filename = path + f'{i}_{j}.jpg'
imwrite(filename, data)
self.data = data
I tried to do the following. But I am getting AttributeError saying Myclass has no attribute 'imwrite'. And I do not know how to mock the nested loops with image writing imwrite.
from mock import MagicMock, patch
def test():
m = MagicMock()
m.__enter__.return_value = data
with patch("h5py.File", return_value=m):
with patch(Myclass.imwrite) as mock_imwrite:
testclass = Myclass()
testclass.load_data(path='test')
assert testclass.get_data() == data
I hope someone can help me out. Any help is very much appreciated
The issue here is that your class Myclass does not have a function imwrite. Only the module (aka the file the class is defined in) is aware of this function.
If you want to patch the imwrite function of the package cv2, you have to write:
(untested code)
with patch('cv2.imwrite') as mock_imwrite:
testclass = Myclass()
...
But the as mock_imwrite part is only needed, if you run an assert on the mock. Otherwise, you may just skip it.

python, calling a method from another class

I am trying to call the sum_method function from my evaluation class to my main one, however I run into many errors. I want to use the new_data as the data parameter of my sum_method function.
evaluation class:
class evaluation():
def __init__(self, data):
self.data = data
def sum_method(self):
montant_init = self.data.loc[self.data['Initiateur'] == 'Glovoapp', 'Montant (centimes)'].sum()
print(montant_init)
main class:
class main(evaluation):
new_data.to_csv("transactions.csv", index=False)
self.data = new_data
def call_sum(self, new_data):
init_eval = evaluation.sum_method(self=new_data)
print(init_eval)
init_evalobj = main()
init_evalobj.call_sum()
if you use the method in your inherence class just use self
so:
init_eval = self.sum_method()
the self argument is passed in python automaticly as first parameter
update
you also should return a value:
def sum_method(self):
montant_init = self.data.loc[self.data['Initiateur'] == 'Glovoapp', 'Montant (centimes)'].sum()
print(montant_init)
return montant_init
I'd suggest making some changes to the both classes, to encapsulate the .data member variable in the base class. My preference would also be to separate out the calculation from the display, so leave all the print statements in the call_sum() function.
class evaluation:
def __init__(self, data):
self.data = data
def sum_method(self):
montant_init = self.data.loc[self.data['Initiateur'] == 'Glovoapp', 'Montant (centimes)'].sum()
return montant_init
class main(evaluation):
def __init__(self):
# Reduce csv content to what's needed for analysis
data_csv = pd.read_csv('transactions.csv')
# --> removing unnecessary data
new_data = data_csv[['Opération', 'Initiateur', 'Montant (centimes)', 'Monnaie',
'Date', 'RĂ©sultat', 'Compte marchand', 'Adresse IP Acheteur', 'Marque de carte']]
# --> saving changes...
new_data.to_csv("transactions.csv", index=False)
super().__init__(new_data) //Initialize the base class
def call_sum(self):
print('Glovoapp "montant" generated')
init_eval = self.sum_method() //Call the method from the base class
print(init_eval)

Is there a way to pass/inject test parameters from outside

Lets say I have the following test class in a tests.py:
class MyTest(unittest.TestCase):
#classmethod
def setUpClass(cls, ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"):
cls.driver = get_remote_webdriver(ip, port, browserType)
cls.driver.implicitly_wait(30)
cls.h5_client_url = h5_client_url
cls.h5_username = h5_username
cls.h5_password = h5_password
#classmethod
def tearDownClass(cls):
cls.driver.quit()
def test_01(self):
# test code
def test_02(self):
# test code
...
def test_N(self):
# test code
All my tests (test_01 to test_N) use the parameters, provided in the setUpClass. Those parameters have default values:
ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"
So I wonder if I can inject new values for those parameters. And I want to do it from another python script so there will be no changes or just minor changes to code of the tests.
Note: I want to run my tests by a batch/shell command and save the output of the test to a log file (to redirect the standard output to that log file)
One think I did was to create a function decorator, that passes a dictionary with key=parameter_name and value=parameter_new_value, but I had to write to much additional code in the tests.py:
I defined the function_decorator logic
I put that #function_decorator annotation above every function I want to decorate
That function decorator needs that dictionary as a parameter, so I made a main, that looks something like that:
if __name__ == '__main__':
# terminal command to run tests should look like this /it is executed by the run-test PARROT command/
# python [this_module_name] [dictionary_containing_parameters] [log_file.log] *[tests]
parser = argparse.ArgumentParser()
# add testbeds_folder as scripts' first parameter, test_log_file as second and tests as the rest
parser.add_argument('dictionary_containing_parameters')
parser.add_argument('test_log_file')
parser.add_argument('unittest_args', nargs='*')
args = parser.parse_args()
dictionary_containing_parameters = sys.argv[1]
test_log_file = sys.argv[2]
# removes the "dictionary_containing_parameters" and "test_log_file" from sys.args - otherwise an error occurs unittest TestRunner
sys.argv[1:] = args.unittest_args
# executes the test/tests and save the output to the test_log_file
with open(test_log_file, "w") as f:
runner = unittest.TextTestRunner(f)
unittest.main(defaultTest=sys.argv[1:], exit=False, testRunner=runner)
Here is one possible solution:
You run your test from a different module this way:
if __name__ == '__main__':
testbed_dict = {"ip": "11.111.111.112",
"browserType": "Chrome",
"port": "4444",
"h5_client_url": "https://new_somelink.com/",
"h5_username": "new_username",
"h5_password": "new_pass"}
sys.argv.append(testbed_dict)
from your_tests_module import *
with open("test.log", "w") as f:
runner = unittest.TextTestRunner(f)
unittest.main(argv=[sys.argv[0]], defaultTest='test_class.test_name', exit=False, testRunner=runner)
You can nottice that argv=[sys.argv[0]] in unittest.main(argv=[sys.argv[0]], defaultTest='test_class.test_name', exit=False, testRunner=runner). Doing that you change the unittests argument to one (no error occurs) to a list with your real arguments. Note that at the end of this list is the dictionary with the new values of test parameters.
Ok, now you write an function decorator, that should look like this:
def load_params(system_arguments_list):
def decorator(func_to_decorate):
#wraps(func_to_decorate)
def wrapper(self, *args, **kwargs):
kwargs = system_arguments_list[-1]
return func_to_decorate(self, **kwargs)
return wrapper
return decorator
And use this decorator this way:
#classmethod
#load_params(sys.argv)
def setUpClass(cls, ip="11.111.111.111",
browserType="Chrome",
port="4444",
h5_client_url="https://somelink.com/",
h5_username="username",
h5_password="pass"):
cls.driver = get_remote_webdriver(ip, port, browserType)
cls.driver.implicitly_wait(30)
cls.h5_client_url = h5_client_url
cls.h5_username = h5_username

python mock global function that is used in class

I can't seem to get my head around mocking in Python. I have a global function:
a.py:
def has_permission(args):
ret_val = ...get-true-or-false...
return ret_val
b.py:
class MySerializer(HyperlinkedModelSerializer):
def get_fields():
fields = super().get_fields()
for f in :
if has_permission(...):
ret_val[f.name] = fields[f]
return ret_val
c.py:
class CountrySerializer(MySerializer):
class Meta:
model = Country
Question: Now i want to test c.py, but i want to mock the has_permission function that is defined in a.py, but is called in the get_fields-method of the class MySerializer that is defined in b.py ... How do i do that?
I've tried things like:
#patch('b.MySerializer.has_permission')
and
#patch('b.MySerializer.get_fields.has_permission')
and
#patch('a.has_permission')
But everything i try either just doesn't work and has_permission is still executed, or python complains about that it can't find the attribute 'has_permission'
with the patching done in:
test.py
class TestSerializerFields(TestCase):
#patch(... the above examples....)
def test_my_country_serializer():
s = CountrySerializer()
self..assertTrue(issubclass(my_serializer_fields.MyCharField, type(s.get_fields()['field1'])))
You need to patch the global in the b module:
#patch('b.has_permission')
because that's where your code looks for it.
Also see the Where to patch section of the mock documentation.
You need to patch the method where it exists at the time your test runs. If you try and patch the method where it is defined after the test code has already imported it, then the patch will have no effect. At the point where the #patch(...) executes, the test code under test has already grabbed the global method into its own module.
Here is an example:
app/util/config.py:
# This is the global method we want to mock
def is_search_enabled():
return True
app/service/searcher.py:
# Here is where that global method will be imported
# when this file is first imported
from app.util.config import is_search_enabled
class Searcher:
def __init__(self, api_service):
self._api_service = api_service
def search(self):
if not is_search_enabled():
return None
return self._api_service.perform_request('/search')
test/service/test_searcher.py:
from unittest.mock import patch, Mock
# The next line will cause the imports of `searcher.py` to execute...
from app.service.searcher import Searcher
# At this point, searcher.py has imported is_search_enabled into its module.
# If you later try and patch the method at its definition
# (app.util.config.is_search_enabled), it will have no effect because
# searcher.py won't look there again.
class MockApiService:
pass
class TestSearcher:
# By the time this executes, `is_search_enabled` has already been
# imported into `app.service.searcher`. So that is where we must
# patch it.
#patch('app.service.searcher.is_search_enabled')
def test_no_search_when_disabled(self, mock_is_search_enabled):
mock_is_search_enabled.return_value = False
mock_api_service = MockApiService()
mock_api_service.perform_request = Mock()
searcher = Searcher(mock_api_service)
results = searcher.search()
assert results is None
mock_api_service.perform_request.assert_not_called()
# (For completeness' sake, make sure the code actually works when search is enabled...)
def test_search(self):
mock_api_service = MockApiService()
mock_api_service.perform_request = mock_perform_request = Mock()
searcher = Searcher(mock_api_service)
expected_results = [1, 2, 3]
mock_perform_request.return_value = expected_results
actual_results = searcher.search()
assert actual_results == expected_results
mock_api_service.perform_request.assert_called_once_with('/search')

Categories

Resources