python: create a test for reading invalid YAML file - python

I am trying to create a test in Python, I want to create a mock for opening YAML file, and mock its content to be an illegal YAML and assert the exception.
I tried mocking:
def test_illegal_yaml_file(self):
with patch('os.path.isfile', return_value=True):
with patch('__main__.open', mock_open(read_data='wifi_password 12345678')):
myObj = MyClass()
params = myObj.get_params()
# TODO catch exception with assert here
and inside MyClass:
def get_params(self):
path = "configuration.yaml"
params = None
if os.path.isfile(path): # os.path.isfile is mocked here to be True
params = self.get_parameters(path)
return params
def get_parameters(self, path):
try:
params = self.load_params_from_yaml(path)
except Exception as e:
Log.error('Invalid YAML file, error: {}'. format(e))
return None
return params
#staticmethod
def yaml_load(camera_configuration):
return yaml.load(camera_configuration, Loader=yaml.FullLoader)
I also tried using IO straight to the inner function:
def test_illegal_yaml_file(self):
un_valid_yaml = io.BytesIO('param 12345678')
params = MyClass.yaml_load(un_valid_yaml)
self.assertIsNone(params)
and inside MyClass:
#staticmethod
def yaml_load(camera_configuration):
return yaml.load(camera_configuration, Loader=yaml.FullLoader)
How can I mock it correctly so an exception of invalid YAML file is thrown?

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.

mocking a class instance in pytest

am trying to mock a class instance while testing a method something like below
source
main_proc.py
devinstance.py
prodinstance.py
requirements.txt
host.json
main_proc.py
def get_instance(self)
ins = None
env = os.getenv('env', 'dev')
if env == 'dev':
ins = DevInstance()
else:
ins = ProdInstance()
return ins
Sample DevInstance class
devinstance.py
class DevInstance:
def __init__(self):
self.eh_client = dict()
self.initialize()
def initialize(self):
try:
client = EventHubProducerClient.from_connection_string(conn_str=self.secrets_dict[value],
eventhub_name=names[i], http_proxy=HTTP_PROXY)
except Exception as e:
logging.error(e)
raise e
testing the get instance like below as my intension is to mock the entire DevInstance class obj. both files are in the same module.
#mock.patch("devinstance.DevInstance")
def test_get_instance(self, devins):
# Act
devins.return_value = MagicMock()
result = get_instance()
# Assert
assert result is not None
Can anyone help me how this can be acheived?
You need to patch where the object is being looked up (see the Where to patch documentation).
If you're testing get_instance in main_proc, then you need to patch where DevInstance is imported in main_proc.
For example, if you're importing it with from devinstance import DevInstance, then you need to patch it with #mock.patch("main_proc.DevInstance").
Otherwise, if you're importing it with import devinstance, then you need to patch it with #mock.patch("main_proc.devinstance.DevInstance").

Python - what's the correct pattern to use for this custom Exception that handles a failed network request?

I have a web service that will return some JSON when a user fails to log in:
{"success":false,"errors":[{"field":"login","msg":"Unknown Login\/Password combination"},{"field":"password"}]}
I'm trying to handle this failure case by raising an exception:
#sl_auth.py
class Error:
def __init__(self, field: str, msg: Optional[str] = None):
self.field = field
self.msg = msg
class SL_Exception(Exception):
def __init__(self, errors: List[Error]):
self.errors = list()
for e in errors:
self.errors.append(Error(**e))
#seems like I could do this more neatly with a map?
#self.errors = list(map(Error.from_json_string, errors))
class SL_Auth:
def __init__(self, success, data):
self.success = success
self.data = Data(**data)
def from_response(response: Response) -> SL_Auth:
#could this be cleaner?
json_obj = response.json()
success = json_obj['success']
if(success==False):
raise SL_Exception(json_obj['errors'])
json_str = response.text
dict = json.loads(json_str)
return SL_Auth(**dict)
It works, but I don't get type hinting for the errors when SL_Exception gets raised.
try:
auth = from_response(response)
except SL_Exception as e:
for error in e.errors:
print(f'SL_Error: {error}') #error is Any, rather than Error
How can I improve this to be a more "pythonic" pattern for handling network requests? Also, is it a better practice to rename Error to SL_Error, or namespace Error where it is imported?

How do I use the functions within this Python script?

I have this Python script to control a PfSense router via FauxAPI. The problem is that when i call a function it gives an error. I think i'm calling the function wrong. Does anyone know how to call them?
Here is a link to the API i'm using: https://github.com/ndejong/pfsense_fauxapi
I have tried calling config_get(self, section=none) but that does not seem to work.
import os
import json
import base64
import urllib
import requests
import datetime
import hashlib
class PfsenseFauxapiException(Exception):
pass
class PfsenseFauxapi:
host = '172.16.1.1'
proto = None
debug = None
version = None
apikey = 'key'
apisecret = 'secret'
use_verified_https = None
def __init__(self, host, apikey, apisecret, use_verified_https=False, debug=False):
self.proto = 'https'
self.base_url = 'fauxapi/v1'
self.version = __version__
self.host = host
self.apikey = apikey
self.apisecret = apisecret
self.use_verified_https = use_verified_https
self.debug = debug
if self.use_verified_https is False:
requests.packages.urllib3.disable_warnings(requests.packages.urllib3.exceptions.InsecureRequestWarning)
def config_get(self, section=None):
config = self._api_request('GET', 'config_get')
if section is None:
return config['data']['config']
elif section in config['data']['config']:
return config['data']['config'][section]
raise PfsenseFauxapiException('Unable to complete config_get request, section is unknown', section)
def config_set(self, config, section=None):
if section is None:
config_new = config
else:
config_new = self.config_get(section=None)
config_new[section] = config
return self._api_request('POST', 'config_set', data=config_new)
def config_patch(self, config):
return self._api_request('POST', 'config_patch', data=config)
def config_reload(self):
return self._api_request('GET', 'config_reload')
def config_backup(self):
return self._api_request('GET', 'config_backup')
def config_backup_list(self):
return self._api_request('GET', 'config_backup_list')
def config_restore(self, config_file):
return self._api_request('GET', 'config_restore', params={'config_file': config_file})
def send_event(self, command):
return self._api_request('POST', 'send_event', data=[command])
def system_reboot(self):
return self._api_request('GET', 'system_reboot')
def system_stats(self):
return self._api_request('GET', 'system_stats')
def interface_stats(self, interface):
return self._api_request('GET', 'interface_stats', params={'interface': interface})
def gateway_status(self):
return self._api_request('GET', 'gateway_status')
def rule_get(self, rule_number=None):
return self._api_request('GET', 'rule_get', params={'rule_number': rule_number})
def alias_update_urltables(self, table=None):
if table is not None:
return self._api_request('GET', 'alias_update_urltables', params={'table': table})
return self._api_request('GET', 'alias_update_urltables')
def function_call(self, data):
return self._api_request('POST', 'function_call', data=data)
def system_info(self):
return self._api_request('GET', 'system_info')
def _api_request(self, method, action, params=None, data=None):
if params is None:
params = {}
if self.debug:
params['__debug'] = 'true'
url = '{proto}://{host}/{base_url}/?action={action}&{params}'.format(
proto=self.proto, host=self.host, base_url=self.base_url, action=action, params=urllib.parse.urlencode(params))
if method.upper() == 'GET':
res = requests.get(
url,
headers={'fauxapi-auth': self._generate_auth()},
verify=self.use_verified_https
)
elif method.upper() == 'POST':
res = requests.post(
url,
headers={'fauxapi-auth': self._generate_auth()},
verify=self.use_verified_https,
data=json.dumps(data)
)
else:
raise PfsenseFauxapiException('Request method not supported!', method)
if res.status_code == 404:
raise PfsenseFauxapiException('Unable to find FauxAPI on target host, is it installed?')
elif res.status_code != 200:
raise PfsenseFauxapiException('Unable to complete {}() request'.format(action), json.loads(res.text))
return self._json_parse(res.text)
def _generate_auth(self):
# auth = apikey:timestamp:nonce:HASH(apisecret:timestamp:nonce)
nonce = base64.b64encode(os.urandom(40)).decode('utf-8').replace('=', '').replace('/', '').replace('+', '')[0:8]
timestamp = datetime.datetime.utcnow().strftime('%Y%m%dZ%H%M%S')
hash = hashlib.sha256('{}{}{}'.format(self.apisecret, timestamp, nonce).encode('utf-8')).hexdigest()
return '{}:{}:{}:{}'.format(self.apikey, timestamp, nonce, hash)
def _json_parse(self, data):
try:
return json.loads(data)
except json.JSONDecodeError:
pass
raise PfsenseFauxapiException('Unable to parse response data!', data)
Without having tested the above script myself, I can conclude that yes you are calling the function wrong. The above script is rather a class that must be instantiated before any function inside can be used.
For example you could first create an object with:
pfsense = PfsenseFauxapi(host='<host>', apikey='<API key>', apisecret='<API secret>')
replacing <host>, <API key> and <API secret> with the required values
Then call the function with:
pfsense.config_get() # self is not passed
where config_get can be replaced with any function
Also note
As soon as you call pfsense = PfsenseFauxapi(...), all the code in
the __init__ function is also run as it is the constructor (which
is used to initialize all the attributes of the class).
When a function has a parameter which is parameter=something, that something is the default value when nothing is passed for that parameter. Hence why use_verified_https, debug and section do not need to be passed (unless you want to change them of course)
Here is some more information on classes if you need.
You need to create an object of the class in order to call the functions of the class. For example
x = PfsenseFauxapi() (the init method is called during contructing the object)
and then go by x.'any function'. Maybe name the variable not x for a good naming quality.

Python testing: using a fake file with mock & io.StringIO

I'm trying to test some code that operates on a file, and I can't seem to get my head around how to replace using a real file with mock and io.StringIO
My code is pretty much the following:
class CheckConfig(object):
def __init__(self, config):
self.config = self._check_input_data(config)
def _check_input_data(self, data):
if isinstance(data, list):
return self._parse(data)
elif os.path.isfile(data):
with open(data) as f:
return self._parse(f.readlines())
def _parse(self, data):
return data
I have a class that can take either a list or a file, if it's a file it opens it and extracts the contents into a list, and then does what it needs to do to the resulting list.
I have a working test as follows:
def test_CheckConfig_with_file():
config = 'config.txt'
expected = parsed_file_data
actual = CheckConfig(config).config
assert expected == actual
I want to replace the call to the filesystem. I have tried replacing the file with io.StringIO but I get a TypeError from os.path.isfile() as it's expecting either a string, bytes or int. I also tried mocking the isfile method like so:
#mock.patch('mymodule.os.path')
def test_CheckConfig_with_file(mock_path):
mock_path.isfile.return_value = True
config = io.StringIO('data')
expected = parsed_file_data
actual = CheckConfig(config).config
assert expected == actual
but I still get the same TypeError as the _io.StringIO type is causing the exception before isfile gets a chance to return something.
How can I get os.path.isfile to return True, when I pass it a fake file? Or is this a suggestion I should change my code?
Just mock out both os.path.isfile and the open() call, and pass in a fake filename (you are not expected to pass in an open file, after all).
The mock library includes a utility for the latter: mock_open():
#mock.patch('os.path.isfile')
def test_CheckConfig_with_file(mock_isfile):
mock_isfile.return_value = True
config_data = mock.mock_open(read_data='data')
with mock.patch('mymodule.open', config_data) as mock_open:
expected = parsed_file_data
actual = CheckConfig('mocked/filename').config
assert expected == actual
This causes the if isinstance(data, list): test to be false (because data is a string instead), followed by the elif os.path.isfile(data): returning True, and the open(data) call to use your mocked data from the mock_open() result.
You can use the mock_open variable to assert that open() was called with the right data (mock_open. assert_called_once_with('mocked/filename') for example).
Demo:
>>> import os.path
>>> from unittest import mock
>>> class CheckConfig(object):
... def __init__(self, config):
... self.config = self._check_input_data(config)
... def _check_input_data(self, data):
... if isinstance(data, list):
... return self._parse(data)
... elif os.path.isfile(data):
... with open(data) as f:
... return self._parse(f.readlines())
... def _parse(self, data):
... return data
...
>>> with mock.patch('os.path.isfile') as mock_isfile:
... mock_isfile.return_value = True
... config_data = mock.mock_open(read_data='line1\nline2\n')
... with mock.patch('__main__.open', config_data) as mock_open:
... actual = CheckConfig('mocked/filename').config
...
>>> actual
['line1\n', 'line2\n']
>>> mock_open.mock_calls
[call('mocked/filename'),
call().__enter__(),
call().readlines(),
call().__exit__(None, None, None)]
In case you end up here wondering how to solve this using the pytest-mock library, here is how you do it:
def test_open(mocker):
m = mocker.patch('builtins.open', mocker.mock_open(read_data='bibble'))
with open('foo') as h:
result = h.read()
m.assert_called_once_with('foo')
assert result == 'bibble'
This code example was found (but had to be adjusted) here.

Categories

Resources