Test for proper exception with Pytest - python

I want to test if a Django Rest Framework returns the proper exception when required, but the test always fails when the exception is raised, instead of succeeding when the exception is the right one:
This is my view:
#api_view(http_method_names=['GET'])
def get_fibonacci(request: Request) -> Response:
"""
Calculates the fibonacci value for number sent as query_param 'n'
"""
try:
n = int(request.query_params['n'])
except ValueError:
raise ValueError("The value of 'n' must be an integer.")
fibonacci_of_n = fibonacci_dynamic_v2(n)
return Response(fibonacci_of_n)
And this is my test:
def test_n_is_a_string_returns_proper_exception(client) -> None:
test_url = f"{fibonacci_url}?n=str"
response = client.get(test_url)
assert response.status_code == 404
And this is the result of the test:
=============================== short test summary info =======================================
FAILED tests/test_api.py::test_n_is_a_string_returns_proper_exception - ValueError: The value of 'n' must be an integer.
Results (1.08s):
23 passed
1 failed
- api/coronavstech/fibonacci_methods/tests/test_api.py:20 test_n_is_a_string_returns_proper_exception
I don't know how to instruct Pytest to succeed the test when the exception is the right one, instead of failing it because there was an exception, which is the result expected from the request.

For catching the exception, you can use pytest.raises() like follows:
def test_n_is_a_string_returns_proper_exception(client) -> None:
test_url = f"{fibonacci_url}?n=str"
with pytest.raises(ValueError):
response = client.get(test_url)
You can even check the exception message:
def test_n_is_a_string_returns_proper_exception(client) -> None:
test_url = f"{fibonacci_url}?n=str"
with pytest.raises(ValueError, match="^The value of 'n' must be an integer\.$"):
response = client.get(test_url)
But the ValueError being thrown inside your view is probably not what you wanted, as it will be shown as a generic 500 error.

Related

SonarQube python - 'not covered by tests' for exception handling and self.fail

I have this unittest:
class GeneralFeaturesV2Test(unittest.TestCase):
def setUp(self) -> None:
self.features_v2_enum = FeatureEnumV2
def test_features_names(self):
bad_features = list()
all_features_names = list(map(lambda value: value.name, self.features_v2_enum))
for feature in all_features_names:
try:
resolve_feature_call(self.features_v2_enum[feature].value.func_name)
except Exception:
bad_features.append(feature)
if bad_features:
self.fail(f'Failed resolving these features: {bad_features}')
For some weird reason sonarqube says that the exception handling and what's inside it and the line of self.fail are not being covered by tests.
And this is the function that I want to test:
def resolve_feature_call(feature_name):
for imp in FEATURES_IMPORT_FILES:
try:
call = getattr(imp, feature_name)
if feature_name is None:
continue
return call
except Exception:
continue
_logger.error("Can't import feature {}".format(feature_name))
raise ValueError("Can't import feature {}".format(feature_name))
What can I do so that it does cover it?
BTW I tried to make it look better like this, but it still failed:
class GeneralFeaturesV2Test(unittest.TestCase):
def setUp(self) -> None:
self.features_v2_enum = FeatureEnumV2
def test_features_names(self):
all_features_names = list(map(lambda value: value.name, self.features_v2_enum))
with self.subTest():
for feature in all_features_names:
try:
resolve_feature_call(self.features_v2_enum[feature].value.func_name)
except Exception:
self.fail(f'Failed resolving the feature: {feature}')

How to create optional tuple return or unpack

I want to send tuple back if function returns error message and the data if success:
Example function:
def test(args):
if (some success here):
return data
return None, error_message
Call:
data, error_msg = test(args)
if error_msg:
return somehttpresponse(error_msg, 400)
return somehttpresponse(data, 200)
How can I achieve this in python? Or is there any other clean way to return error message without compromising the return data on success?
So far I used Exception:
Example function:
def test(args):
if (some success here):
return data
raise ValueError(error_message)
return None
Call:
try:
data = test(args)
return somehttpresponse(data, 200)
except ValueError as err:
return somehttpresponse(err, 400)
Not sure if it's the most efficient way. still hoping someone has an answer

Pythonic way to get value defined or not defined in yaml

I have a yaml file with test(s) configuration(s) and there is an optional parameter "ignore-dup-txn" in optional section "test-options".
test-name:
test-type: trh_txn
test-src-format: excel
test-src-excel-sheet: invalid_txns
test-options:
ignore-dup-txn: True
I read section "test-name" to "test" dict and for now I check it this way:
if 'test-options' in test and 'ignore-dup-txn' in test['test-options']:
ignore_dups = test['test-options']['ignore-dup-txn']
else:
ignore_dups = None
What would be the pythonic way to do it? More clear, simple and shorter.
I was thinking to do "getter", but if I do get(test['test-option']['ignore-dup-txn']), I will get an exception in case if option is not defined, obviously.
You can use the get method:
test['test-options'].get('ignore-dup-txn',default-value)
This would work:
test.get('test-options', {}).get('ignore-dup-txn', None)
If you just want a "one-liner" and don't want an empty dict to be created you can do:
ignore_dups = test['test-options'].get('ignore-dup-txn') if 'test-options' in test else None
but this leads to long lines, and doesn't expand well to another level and is not very pythonic.
For something that is IMO a more pythonic first look at what happens when you have a dict and use a list as key for assignment or as first argument to .get() ¹:
d = dict()
l = ['a', 'b', 'c']
try:
d[l] = 3
except TypeError as e:
assert e.message == "unhashable type: 'list'"
else:
raise NotImplementedError
try:
d.get(l, None)
except TypeError as e:
assert e.message == "unhashable type: 'list'"
else:
raise NotImplementedError
That means some_dict.get(['a', 'b', 'c'], default) will throw a TypeError. on the other hand that is rather a nice concise syntax to get a value from a dict within a dict within ... .
So the question becomes how can I get such a .get() to work?
First you have to realise you cannot just replace the .get() method on a dict, you'll get an AttributeError:
d = dict()
def alt_get(key, default):
pass
try:
d.get = alt_get
except AttributeError as e:
assert e.message == "'dict' object attribute 'get' is read-only"
else:
raise NotImplementedError
So you will have to subclass dict, this allows you to override the .get() method:
class ExtendedDict(dict):
def multi_level_get(self, key, default=None):
if not isinstance(key, list):
return self.get(key, default)
# assume that the key is a list of recursively accessible dicts
# *** using [] and not .get() in the following on purpose ***
def get_one_level(key_list, level, d):
if level >= len(key_list):
if level > len(key_list):
raise IndexError
return d[key_list[level-1]]
return get_one_level(key_list, level+1, d[key_list[level-1]])
try:
return get_one_level(key, 1, self)
except KeyError:
return default
get = multi_level_get # delete this if you don't want to mask get()
# you can still use the multi_level-get()
d = dict(a=dict(b=dict(c=42)))
assert d['a']['b']['c'] == 42
try:
d['a']['xyz']['c'] == 42
except KeyError as e:
assert e.message == 'xyz'
else:
raise NotImplementedError
ed = ExtendedDict(d)
assert ed['a']['b']['c'] == 42
assert ed.get(['a', 'b', 'c'], 196) == 42
assert ed.get(['a', 'xyz', 'c'], 196) == 196 # no execption!
This works fine when only having dicts within dicts recursively, but also to a limited extend when you mix these with lists:
e = dict(a=[dict(c=42)])
assert e['a'][0]['c'] == 42
ee = ExtendedDict(e)
# the following works becauuse get_one_level() uses [] and not get()
assert ee.get(['a', 0, 'c'], 196) == 42
try:
ee.get(['a', 1, 'c'], 196) == 42
except IndexError as e:
assert e.message == 'list index out of range'
else:
raise NotImplementedError
try:
ee.get(['a', 'b', 'c'], 196) == 42
except TypeError as e:
assert e.message == 'list indices must be integers, not str'
else:
raise NotImplementedError
You can of course catch the latter two errors as well in multi_level_get() by using except (KeyError, TypeError, IndexError): and returning the default
for all these cases.
In ruamel.yaml ² this multi-level-get is implemented as mlget() (which requires an extra parameter to allow lists to be part of the hierarchy):
import ruamel.yaml as yaml
from ruamel.yaml.comments import CommentedMap
yaml_str = """\
test-name:
test-type: trh_txn
test-src-format: excel
test-src-excel-sheet: invalid_txns
test-options:
ignore-dup-txn: True
"""
data = yaml.load(yaml_str, Loader=yaml.RoundTripLoader)
assert data['test-name']['test-options']['ignore-dup-txn'] is True
assert data.mlget(['test-name', 'test-options', 'ignore-dup-txn'], 42) is True
assert data.mlget(['test-name', 'test-options', 'abc'], 42) == 42
print(data['test-name']['test-src-format'])
which prints:
excel
¹ In the examples I rather use assertions to confirm what is happening than print statements and then separate explanations on what gets printed. This keeps the explanation more conscise, and in the case of assertions within try/except blocks clear that the exception was thrown, without breaking the code and prohibiting following code from being executed. All of the example example code comes from python file that runs and only prints one value.
² I am the author of that package, which is an enhanced version of PyYAML.

Same Exceptions with different specific message

I tried to write a code that can distinguish the following four different errors.
TypeError: The first parameter is not an integer;
TypeError: The second parameter is not a string;
ValueError: The value of the first parameter is not in the range of 1 to 13; or
ValueError: The value of the second parameter is not one of the strings in the set {'s', 'h', 'c', 'd'}.
However, I only can get the first one to work but not the other three errors. I tried different ways to make it work, but still can't figure out what's wrong.
class Card: # One object of class Card represents a playing card
rank = ['','Ace','Two','Three','Four','Five','Six','Seven','Eight','Nine','Ten','Jack','Queen','King']
suit = {'d':'Diamonds', 'c':'Clubs', 'h':'Hearts', 's':'Spades'}
def __init__(self, rank=2, suit=0): # Card constructor, executed every time a new Card object is created
if type(rank) != int:
raise TypeError()
if type(suit) != str:
raise TypeError()
if rank != self.rank:
raise ValueError()
if suit != 'd' or 'c' or 'h' or 's':
raise ValueError()
self.rank = rank
self.suit = suit
def getRank(self): # Obtain the rank of the card
return self.rank
def getSuit(self): # Obtain the suit of the card
return Card.suit[self.suit]
def bjValue(self): # Obtain the Blackjack value of a card
return min(self.rank, 10)
def __str__(self): # Generate the name of a card in a string
return "%s of %s" % (Card.rank[int(self.rank)], Card.suit[self.suit])
if __name__ == "__main__": # Test the class Card above and will be skipped if it is imported into separate file to test
try:
c1 = Card(19,13)
except TypeError:
print ("The first parameter is not an integer")
except TypeError:
print ("The second parameter is not a string")
except ValueError:
print ("The value of first parameter is not in the range of 1 to 13")
except ValueError:
print ("The value of second parameter is not one of the strings in the set {'s','h','c','d'}")
print(c1)
I know maybe it is due to that I have same TypeError and ValueError. Therefore, Python can't distinguish the second TypeError which I hit c1 = Card(13,13) is different from the first TypeError. So, I only get the message that "The first parameter is not an integer" when I have c1 = Card(13,13).
You are trying to distinguish between the error sources in completely the wrong place. By the time the error gets out of Card.__init__, there is no way to tell why e.g. a TypeError was thrown. For each error class (TypeError, ValueError) only the first except will ever be triggered:
try:
...
except TypeError:
# all TypeErrors end up here
except TypeError:
# this is *never* reached
except ValueError:
# all ValueErrors end up here
except ValueError:
# this is *never* reached
Instead, you should provide the specific error messages inside Card.__init__, when you actually raise the error and already know what the reason is:
if not isinstance(rank, int): # better than comparing to type
raise TypeError("The first parameter is not an integer")
Then you can handle them much more simply:
try:
c1 = Card(19,13)
except (TypeError, ValueError) as err: # assign the error to the name 'err'
print(err) # whichever we catch, show the user the message
else:
print(c1) # only print the Card if there were no errors
If you have a particular need to distinguish between different errors of the same class, you can either explicitly check the message:
except TypeError as err:
if err.args[0] == "The first parameter is not an integer":
# do whatever you need to
or create your own, more specific Exception sub-classes, so you can have separate except blocks:
class FirstParamNotIntError(Exception):
pass
(this is just an example, they are too specific in your particular case).
It's probably worth having a read through the documentation on exceptions and the tutorial on using them.

Putting custom error messages in Python

I want to create custom error messages for a function.
def tr( launch_speed , launch_angle_deg , num_samples ):
#Error displays
try:
launch_speed>0
except:
raise Exception("Launch speed has to be positive!")
try:
0<launch_angle_deg<90
except:
raise Exception("Launch angle has to be 0 to 90 degrees!")
try:
um_samples = int(input())
except:
raise Exception("Integer amount of samples!")
try:
num_samples >=2
except:
raise Exception("At least 2 samples!")
Essentially, what I want is to get an error message every time a wrong value has been written in the function variables, and I've tried creating these messages based on what I've gathered on the Internet, but it doesn't seem to work.
You can't use try: except: for everything; for example, launch_speed>0 will not raise an error for negative values. Instead, I think you want e.g.
if launch_speed < 0: # note spacing, and if not try
raise ValueError("Launch speed must be positive.") # note specific error
You should also test for and raise more specific errors (see "the evils of except"), e.g.:
try:
num_samples = int(raw_input()) # don't use input in 2.x
except ValueError: # note specific error
raise TypeError("Integer amount of samples!")
You can see the list of built-in errors in the documentation.
Why not go one step further and build your own exception types? There's a quick tutorial in the docs which could be used something like:
class Error(Exception):
"""Base class for exceptions defined in this module"""
pass
class LaunchError(Error):
"""Errors related to the launch"""
pass
class LaunchSpeedError(LaunchError):
"""Launch speed is wrong"""
pass
class LaunchAngleError(LaunchError):
"""Launch angle is wrong"""
pass
class SamplesError(Error):
"""Error relating to samples"""
pass
In this case the default functionality of Exception is fine, but you may be able to get finer granularity in what you catch by defining extra exceptions.
if launch_speed < 0:
raise LaunchSpeedError("Launch speed must be positive")
if 0 <= launch_angle < 90:
raise LaunchAngleError("Launch angle must be between 0 and 90")
um_samples = input()
try:
um_samples = int(um_samples)
except ValueError:
raise SampleError("Samples must be an integer, not {}".format(um_samples))
if um_samples < 2:
raise SampleError("Must include more than one sample, not {}".format(str(um_samples)))

Categories