Search code examples
pythonunit-testingpython-decorators

Test decorator parameters


I have a decorator that accept two parameters callback and onerror, both should be callables something like this

class mydecorator(object):
    def __init__(self, callback, onerror=None):
        if callable(callback):
            self.callback = callback
        else:
            raise TypeError('Callable expected in "callback" parameter')

        self.onerror = onerror
        if self.onerror and not callable(self.onerror):
            raise TypeError('Callable expected in "onerror" parameter')

    def __call__(self, func):
        return self.__param__call__(func)

    def __param__call__(self, func):
        def wrapper(*args, **kwargs):
            result = func(*args, **kwargs)
            try:
                self.callback()
            except MyCustomException as e:
                if self.onerror:
                    self.onerror(e.message, e.data)
                else:
                    raise

            return result 
        return wrapper

I would like to test with I pass a invalid parameter, for example a non callable, it should raise a TypeError

Using Python unittest what is the best approach to achieve this? I'm willing to do something like:

def test_non_callable_callback_should_return_type_error(self):
    try:
        @mydecorator('this_is_not_a_callable')
        def my_phony_func():
            pass
    except TypeError:
        # Correctly has raised a TypeError, lets just pass
        pass
    else:
        # It has not raised an TypeError, let's fail
        self.fail('TypeError not raised when a non callable passed to callback')

It must be a better way, didn't?


Solution

  • As Leo K commented, there's a much easier way to test if some code raises an exception in a test, though different libraries spell things slightly differently. In a unittest.TestCase, you can use self.assertRaises as a context manager:

    def test_non_callable_callback_should_return_type_error(self):
        with self.assertRaises(TypeError):
            @mydecorator('this_is_not_a_callable')
            def my_phony_func():
                pass
    

    In fact, you could simplify things even more by doing away with unnecessary parts of the code being tested. Since you expect the mydecorator class to raise the exception when it is called (rather than when the instance is called on the function), you can get rid of the dummy function and skip the @decorator syntax all together. The assertRaises method can even make the call for you:

    def test_non_callable_callback_should_return_type_error(self):
        self.assertraises(TypeError, mydecorator, 'this_is_not_a_callable')