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.