I'm writing some unit tests for a RabbitMQ service I've developed and I'm trying to catch an AMQP exception when the exchange doesn't exist.
The method prototype for the RabbitMQ class I've written is publish_message_str(self, exchange_name: str, message: dict) -> None.
I am trying to unit test several things, and the reason for this question is one of those tests giving an exception I am unable to "expect".
# service: RabbitMQ is defined in a function-scoped fixture
def test_publish_unexistent_exchange(service: RabbitMQ) -> None:
service.publish_message_str("exchangenotexists", {})
When the code is run, it raises an expected exception.
E amqp.exceptions.NotFound: Basic.publish: (404) NOT_FOUND - no exchange 'exchangenotexists' in vhost '/'
In a regular situation, you could do something like this:
def test_publish_unexistent_exchange(service: RabbitMQ) -> None:
with raises(NotFound):
service.publish_message_str("exchangenotexists", {})
This however also gives the exception and pytest seems to ignore the raises call.
I've also tried to catch a general exception with a try-except.
def test_publish_unexistent_exchange(service: RabbitMQ) -> None:
try:
service.publish_message_str("exchangenotexists", {})
except Exception:
pass
This doesn't work either. The next thing I've tried is reading the AMQP exceptions code, but unfortunately at this point I'm clueless.
If you have faced this situation and/or know how to solve it, I'd deeply appreciate it.
Thank you so much,
Related
I was wondering how I could alter a pytest test outcome (from a fail to a skip) in the case that my error message includes a specific string.
Occasionally we get test failures using appium where the response from the appium server is a 500 error with the failure message: "An unknown server-side error occurred while processing the command." Its an issue that we need to solve, but for the meantime we want to basically say, if the test failed because of an error message similar to that, skip the test instead of failing it.
Ive considered and tried something like this:
def pytest_runtest_setup(item):
excinfo = None
try:
item.obj()
except Exception as e:
excinfo = sys.exc_info()
if excinfo and "An unknown server-side error occurred while processing the command." in str(excinfo[1]):
pytest.skip("Skipping test due to error message")
And this obviously won't work.
But I was hoping for a similar approach.
The successful answer:
In order for me to get this working #Teejay pointed out below I needed to use the runtest_call hook and assess the message there. Currently working well in my test suite!
#pytest.hookimpl(hookwrapper=True)
def pytest_runtest_call(item):
output = yield
if output.excinfo:
for potential_error_message in expected_failure_messages.keys():
if output._excinfo[1].stacktrace and potential_error_message in output._excinfo[1].stacktrace:
pytest.skip(reason=expected_failure_messages[potential_error_message])
I recommend utilizing a hook wrapper to inspect the exception raised and act accordingly
#pytest.hookimpl(hookwrapper=True)
def pytest_runtest_setup(item):
output = yield
if output.excinfo:
# Additional logic to target specific error
pytest.skip()
Your sketched idea is close to workable. I'd skip using sys.exc_info() and just inspect the str() value of the exception, and I'd restrict the classes of exception caught to the smallest set that covers the failures you're trying to ignore.
Something like:
try:
item.obj()
except OSError as e: # ideally a narrower subclass or tuple of classes
if 'An unknown server-side error occurred while processing the command.' in str(e):
pytest.skip(f'Skipping test due to error message {e}')
else:
raise e
The only additional logical change I've made is moving the if/skip into the exception handler and re-raising if it doesn't match the message you're expecting.
Restricting the classes of errors matched is a best practice to avoid catching situations you didn't intend to catch. It's probably harmless to over-catch here where you're inspecting the message, just a good habit to cultivate. But it might also let you identify a specific field of the exception class to check rather than just the string representation - eg OSError has the strerror attribute containing the error message from the OS, so if you've limited your except block to catching just those you know you'll have that attribute available.
I chose to include the exception in the skip message, you might decide differently if they're uninformative.
I have a program with some low-level hardware components, which may fail(not initialized, timeout, comm issues, invalid commands etc.). They live in a server, which receives requests from a webclient.
So my idea is to have custom exceptions to capture what may fail in which drive - so that I can in some cases take remediation actions (e.g. try to reset the adapter if it's a comm problem etc.), or bubble up the errors in the cases where I can't do anything low-level, perhaps so that the server can return a generic error message to the webclient.
For instance:
class DriveException(Exception):
""" Raised when we have a drive-specific problem """
def __init__(self, message, drive=None, *args):
self.message = message
self.drive = drive
super().__init__(message, drive, *args)
But then that drive may have had a problem because, say, the ethernet connexion didn't respond:
class EthernetCommException(Exception):
""" Raised when ethernet calls failed """
In the code, I can ensure my exceptions bubble up this way:
# ... some code ....
try:
self.init_controllers() # ethernet cx failed, or key error etc.
except Exception as ex:
raise DriveException(ex) from ex
# .... more code....
I have a high-level try/except in the server to ensure it keeps responding to requests & doesn't crash in case of a low-level component not responding. That mechanic works fine.
However, I have many different drives. I'd rather avoid putting lots of try/except everywhere in my code. My current idea is to do something like:
def koll_exception(func):
""" Raises a drive exception if needed """
#functools.wraps(func)
def wrapper_exception(*args, **kwargs):
try:
value = func(*args, **kwargs)
return value
except Exception as ex:
raise DriveException(ex, drive=DriveEnum.KOLLMORGAN) from ex
return wrapper_exception
So that I can just dO:
#koll_exception
def risky_call_to_kolldrive():
#doing stuff & raising a drive exception if anything goes wrong
# then anywhere in the code
foo = risky_call_to_kolldrive()
My prototype seems to work fine with the decorator. However I've search a bit about using to approach to try/except and was somewhat surprise not to find much about it. Is there a good reason why people don't do that I'm not seeing? Other than they usually just wrap everything in a high-level try/catch & don't bother much more with it?
I am scratching my head about what is the best-practice to get the traceback in the logfile only once. Please note that in general I know how to get the traceback into the log.
Let's assume I have a big program consisting of various modules and functions that are imported, so that it can have quite some depth and the logger is set up properly.
Whenever an exception may occur I do the following:
try:
do_something()
except MyError as err:
log.error("The error MyError occurred", exc_info=err)
raise
Note that the traceback is written to the log via the option exc_info=err.
My Problem is now that when everything gets a bit more complex and nested I loose control about how often this traceback is written to the log and it gets quite messy.
An example of the situation with my current solution for this problem is as follows:
from other_module import other_f
def main():
try:
# do something
val = other_f()
except (AlreadyLoggedError1, AlreadyLoggedError2, AlreadyLoggedError3):
# The error was caught within other_f() or deeper and
# already logged with traceback info where it occurred
# After logging it was raised like in the above example
# I do not want to log it again, so it is just raised
raise
except BroaderException as err:
# I cannot expect to have thought of all exceptions
# So in case something unexpected happened
# I want to have the traceback logged here
# since the error is not logged yet
log.error("An unecpected error occured", exc_info=err)
raise
The problem with this solution is, that I need to to keep track of all Exceptions that are already logged by myself and the line except (AlreadyLoggedError1, AlreadyLoggedError2, ...) gets arbitrary long and has to be put at any level between main() and the position the error actually occured.
So my question is: Is there some better (pythonic) way handling this? To be more specific: I want to raise the information that the exception was already logged together with the exception so that I do not have to account for that via an extra except block like in my above example.
The solution normally used for larger applications is for the low-level code to not actually do error handling itself if it's just going to be logged, but to put exception logging/handling at the highest level in the code possible, since exceptions will bubble up as far as needed. For example, libraries that send errors to a service like New Relic and Sentry don't need you to instrument each small part of your code that might throw an error, they are set up to just catch any exception and send it to a remote service for aggregation and tracking.
I have been writing unittests. Although there must be a way, I find it exceptionally difficult to get any print statement in a unittest class method to print anything, but can see what is going on when unittest shows me the diffs.
Have previously tested for exceptions with assertRaises as described here, but this is about detecting not asserting.
The tests assume a certain hardware device is plugged into the USB port when they run. The program being tested raises the following if someone tries to run it without the device connected:
RuntimeError('Device not reachable. Please connect it to USB')
I just tested sys.exit(0) inside a unittest method and that didn't exit either. Still, exiting is not that important, is probably better if all the tests fail, but it would be polite if the first test detects that Exception then prints a message to the user such as "The device needs to be connected before running unittests.". Does anyone know how to achieve this?
It is the standard class as described in the docs inheriting from unittest.TestCase with a setUp and tearDown method and the tests in between.
There are a few solutions you could try:
log and raise your RuntimeError in the setUpClass method
Use the skipUnless decorator
Intercept every error raised to test its type and log if it is a RuntimeError (see below)
To intercept every exception raised:
SYS_EXCEPT_HOOK = sys.excepthook
def _excepthook(typ, value, trace):
if typ is RuntimeError:
print("{}\n{}\n{}".format(typ.__name__, value, ''.join(traceback.format_tb(trace))))
SYS_EXCEPT_HOOK(typ, value, trace)
sys.excepthook = _excepthook
When performing async urlfetch calls with a callback and inside a tasklet, it seems that exceptions raised from within the callback don't propagate to the wrapping tasklet.
Example code:
def cb() :
raise Exception, 'just a test'
rpc = urlfetch.create_rpc(callback = cb)
#ndb.tasklet
def t() :
try :
response = yield urlfetch.make_fetch_call(rpc, 'http://...')
except :
print 'an error occured'
raise ndb.Return
t().get_result()
In the code above, executed by the dev server, the "just a test" exception doesn't get caught inside the tasklet; ie. instead of the error message being output to the console i'm getting the "just a test" exception reported.
If there's a generic urlfetch exception related to the make_fetch_call (such as DownloadError in case of a bad URL), it's being handled properly.
Is there a way to catch callback-generated exceptions inside the tasklet in such a situation? Or maybe should this behavior be considered a bug?
Thanks.
I've created a sample project to illustrate the correct way to do this.
While reading the code, you'll find a lot of benefit from reading the comments and also cross-referencing with the docs on tasklets and rpc.make_fetch_call().
Some of the confusing aspects of this were the fact that ndb tasklets actually use Exceptions to indicate return values (even if successful, you should raise ndb.Return (True), making exception handling difficult to tiptoe around), and the fact that exceptions in the callback needs to be caught when we call wait() on the rpc future object returned by t(), while exceptions in the url fetch need to be caught inside t() itself when we do yield rpc.make_fetch_call(). There may be a way to do the latter using rpc.check_success(), but that'll be up to your hacking to figure out.
I hope you find the source helpful, and I hope you learned a lesson about avoiding using exceptions to indicate that a generator is done...