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.
Something odd happens when test_decorated is renamed.
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):
and now both run again.
Thanks for the tip. Any clue on why this makes a difference?
unittest needs the function to begin with the word “test” (seriously) and @wraps maintains the __name__.
how do i skip the test cases if previous one is failed?
Seems to me like you have a bigger problem then. One test should never depend on another test passing/failing so you need to redesign your test design and/or approach.