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

"""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:

"""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:

$ 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:

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.

This entry was posted in Development and tagged , . Bookmark the permalink. Trackbacks are closed, but you can post a comment.

3 Comments

  1. Andy
    Posted July 11, 2013 at 20:31 | Permalink

    Something odd happens when test_decorated is renamed.

    •  def test_decorated(self, *args, **kwargs):
      
    •  def xtest_decorated(self, *args, **kwargs):
      
    •  return test_decorated
      
    •  return xtest_decorated>
      

    When you run the first MyTestCase, it will not run any of the tests. The solution to this is to use @wraps in the decorator.

    from functools import wraps

    def expect_exception(exception):

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

    and now both run again.

    • drm
      Posted July 16, 2013 at 17:46 | Permalink

      Thanks for the tip. Any clue on why this makes a difference?

    • Marshall
      Posted October 14, 2015 at 21:29 | Permalink

      unittest needs the function to begin with the word “test” (seriously) and @wraps maintains the __name__.

Post a Comment

Your email is never published nor shared.

You may use these HTML tags and attributes <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

Subscribe without commenting