Tinker
Tinker

Reputation: 4555

How to disable a try/except block during testing?

I wrote a cronjob that iterates through a list of accounts and performs some web call for them (shown below):

for account in self.ActiveAccountFactory():
  try:
    self.logger.debug('Updating %s', account.login)
    self.update_account_from_fb(account)
    self.check_balances()
    self.check_rois()
  except Exception,e:
    self.logger.exception(traceback.format_exc())

Because this job is run by heroku one every 10 minutes, I do not want the entire job to fail just because one account is running into issues (it happens). I placed a try catch clause here so that this task is "fault-tolerant".

However, I noticed that when I am testing, this try/catch block is giving me cryptic problems because of the task is allowed to continue executing even though there is some serious error.

What is the best way to disable a try/except block during testing?

I've though about implementing the code directly like this:

for account in self.ActiveAccountFactory():
    self.logger.debug('Updating %s', account.login)
    self.update_account_from_fb(account)
    self.check_balances()
    self.check_rois()
    self.logger.exception(traceback.format_exc())

in my test cases but then this makes my tests very clumsy as I am copying large amounts of code over.

What should I do?

Upvotes: 1

Views: 2491

Answers (3)

Rick
Rick

Reputation: 45291

First of all: don't swallow all exceptions using except Exception. It's bad design. So cut it out.

With that out of the way:

One thing you could do is setup a monkeypatch for the logger.exception method. Then you can handle the test however you see fit based on whether it was called, whether it's creating a mock logger, or a separate testing logger, or a custom testing logger class that stops the tests when certain exceptions occur. You could even choose to end the testing immediately by raising an error.

Here is an example using pytest.monkeypatch. I like pytest's way of doing this because they already have a predefined fixture setup for it, and no boilerplate code is required. However, there are others ways to do this as well (such as using unittest.mock.patch as part of the unitest module).

I will call your class SomeClass. What we will do is create a patched version of your SomeClass object as a fixture. The patched version will not log to the logger; instead, it will have a mock logger. Anything that happens to the logger will be recorded in the mock logger for inspection later.

import pytest
import unittest.mock as mock # import mock for Python 2

@pytest.fixture
def SomeClassObj_with_patched_logger(monkeypatch):
    ##### SETUP PHASE ####
    # create a basic mock logger: 
    mock_logger = mock.Mock(spec=LoggerClass)
    # patch the 'logger' attribute so that when it is called on 
    # 'some_class_instance' (which is bound to 'self' in the method)
    # things are re-routed to mock_logger
    monkeypatch.setattr('some_class_instance.logger', mock_logger)
    # now create class instance you will test with the same name
    # as the patched object
    some_class_instance = SomeClass()
    # the class object you created will now be patched
    # we can now send that patched object to any test we want
    # using the standard pytest fixture way of doing things
    yield some_class_instance
    ###### TEARDOWN PHASE #######
    # after all tests have been run, we can inspect what happened to
    # the mock logger like so: 
    print('\n#### ', mock_logger.method_calls)        

If call.exception appears in the method calls of the mock logger, you know that method was called. There are a lot of other ways you could handle this as well, this is just one.

If you're using the logging module, LoggerClass should just be logging.Logger. Alternatively, you can just do mock_logger = mock.Mock(). Or, you could create your own custom testing logger class that raises an exception when its exception method is called. The sky is the limit!

Use your patched object in any test like so:

def test_something(SomeClassObj_with_patched_logger):
    # no need to do the line below really, just getting
    # a shorter variable name
    my_obj = SomeClassObj_with_patched_logger
    #### DO STUFF WITH my_obj #####

If you are not familiar with pytest, see this training video for a little bit more in depth information.

Upvotes: 1

Terry Jan Reedy
Terry Jan Reedy

Reputation: 19184

You can make callables test-aware by add a _testing=False parameter. Use that to code alternate pathways in the callable for when testing. Then pass _testing=True when calling from a test file.

For the situation presented in this question, putting if _testing: raise in the exception body would 'uncatch' the exception.

Conditioning module level code is tricker. To get special behavior when testing module mod in package pack, I put

_testing = False  # in `pack.__init__`

from pack import _testing  # in pack.mod

Then test_mod I put something like:

import pack
pack._testing = True
from pack import mod

Upvotes: 0

BoarGules
BoarGules

Reputation: 16951

try...except blocks are difficult when you are testing because they catch and try to dispose of errors you would really rather see. As you have found out. While testing, for

except Exception as e:

(don't use Exception,e, it's not forward-compatible) substitute an exception type that is really unlikely to occur in your circumstances, such as

except AssertionError as e:

A text editor will do this for you (and reverse it afterwards) at the cost of a couple of mouse-clicks.

Upvotes: 0

Related Questions