melp.nl

< Return to main page

Enhancing Python unit tests further with decorators

Decorators in Python are awesome. In follow-up to my previous post on a missing feature in Python's unittest module in comparison to PHPUnit, here's the implementation of PHPUnit's @expectedException annotation in form of a Python decorator

#!python
"""test.py, demonstrating the @expected_exception decorator"""
import unittest

def expect_exception(exception):
    """Marks test to expect the specified exception. Call assertRaises internally"""
    def test_decorator(fn):
        def test_decorated(self, *args, **kwargs):
            self.assertRaises(exception, fn, self, *args, **kwargs)
        return test_decorated
    return test_decorator

class MyTestCase(unittest.TestCase):
    @expect_exception(ValueError)
    def test_value_error(self):
        int("A") # test succeeds

    @expect_exception(ValueError)
    def test_value_error(self):
        int("0") # test fails

Running this test would have the second test fail, because it doesn't raise a ValueError.

Combining this with a slightly enhanced version of the @data_provider decorator will work without trouble:

#!python
"""test2.py, demonstrating the @expected_exception in combination with the @data_provider decorator"""
def data_provider(data):
    """Data provider decorator, allows a callable to provide the data for the test"""
    if callable(data):
        data = data()

    if not all(isinstance(i, tuple) for i in data):
        raise Exception("Need a sequence of tuples as data...")

    def test_decorator(fn):
        def test_decorated(self, *args):
            for i in data:
                try:
                    fn(self, *(i + args))
                except AssertionError as e:
                    raise AssertionError(e.message + " (data set used: %s)" % repr(i))
        return test_decorated
    return test_decorator

def expect_exception(exception):
    """Marks test to expect the specified exception. Call assertRaises internally"""
    def test_decorator(fn):
        def test_decorated(self, *args, **kwargs):
            self.assertRaises(exception, fn, self, *args, **kwargs)
        return test_decorated
    return test_decorator

class MyTestCase(unittest.TestCase):

    @data_provider((("A",), ("0",),))
    @expect_exception(ValueError)
    def test_value_error(self, value):
        int(value) # test fails on second data set

Output:

#!shell
$ python -m unittest test2
F
======================================================================
FAIL: test_value_error (test2.MyTestCase)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test.py", line 18, in test_decorated
    raise AssertionError(e.message + " (data set used: %s)" % repr(i))
AssertionError: ValueError not raised (data set used: ('0',))

----------------------------------------------------------------------
Ran 1 test in 0.000s

FAILED (failures=1)

Note that the actual test code is very concise:

#!python
class MyTestCase(unittest.TestCase):
    @data_provider((("A",), ("0",),))
    @expect_exception(ValueError)
    def test_value_error(self, value):
        int(value) # test fails on second data set

Gotta love that language :) The code applied in the unit tests for srtfix.py.


< Return to main page


You're looking at a very minimalistic archived version of this website. I wish to preserve to content, but I no longer wish to maintain Wordpress, nor handle the abuse that comes with that.