如何使用 Python 的单元测试来测试是否抛出了警告?

我在 Python 中有一个下面的函数,我想用 unittest 测试一下,如果函数的参数为0,它会抛出一个警告。我已经试过 assertRaises 了,但是因为我没有提出警告,所以不管用。

def isZero(i):
if i != 0:
print "OK"
else:
warning = Warning("the input is 0!")
print warning
return i
28483 次浏览

You can use the catch_warnings context manager. Essentially this allows you to mock the warnings handler, so that you can verify details of the warning. See the official docs for a fuller explanation and sample test code.

import warnings


def fxn():
warnings.warn("deprecated", DeprecationWarning)


with warnings.catch_warnings(record=True) as w:
# Cause all warnings to always be triggered.
warnings.simplefilter("always")
# Trigger a warning.
fxn()
# Verify some things
assert len(w) == 1
assert issubclass(w[-1].category, DeprecationWarning)
assert "deprecated" in str(w[-1].message)

@ire_and_curses' answer is quite useful and, I think, canonical. Here is another way to do the same thing. This one requires Michael Foord's excellent Mock library.

import unittest, warnings
from mock import patch_object


def isZero( i):
if i != 0:
print "OK"
else:
warnings.warn( "the input is 0!")
return i


class Foo(unittest.TestCase):
@patch_object(warnings, 'warn')
def test_is_zero_raises_warning(self, mock_warn):
isZero(0)
self.assertTrue(mock_warn.called)


if __name__ == '__main__':
unittest.main()

The nifty patch_object lets you mock out the warn method.

You can write your own assertWarns function to incapsulate catch_warnings context. I've just implemented it the following way, with a mixin:

class WarningTestMixin(object):
'A test which checks if the specified warning was raised'


def assertWarns(self, warning, callable, *args, **kwds):
with warnings.catch_warnings(record=True) as warning_list:
warnings.simplefilter('always')


result = callable(*args, **kwds)


self.assertTrue(any(item.category == warning for item in warning_list))

A usage example:

class SomeTest(WarningTestMixin, TestCase):
'Your testcase'


def test_something(self):
self.assertWarns(
UserWarning,
your_function_which_issues_a_warning,
5, 10, 'john', # args
foo='bar'      # kwargs
)

The test will pass if at least one of the warnings issued by your_function is of type UserWarning.

Starting with Python 3.2, you can simply use assertWarns() method.

with self.assertWarns(Warning):
do_something()

One problem with the warnings.catch_warnings approach is that warnings produced in different tests can interact in strange ways through global state kept in __warningregistry__ attributes.

To address this, we should clear the __warningregistry__ attribute of every module before every test that checks warnings.

class MyTest(unittest.TestCase):


def setUp(self):
# The __warningregistry__'s need to be in a pristine state for tests
# to work properly.
for v in sys.modules.values():
if getattr(v, '__warningregistry__', None):
v.__warningregistry__ = {}


def test_something(self):
with warnings.catch_warnings(record=True) as w:
warnings.simplefilter("always", MySpecialWarning)
...
self.assertEqual(len(w), 1)
self.assertIsInstance(w[0].message, MySpecialWarning)


This is how Python 3's assertWarns() method is implemented.

Building off the answer from @ire_and_curses,

class AssertWarns(warnings.catch_warnings):
"""A Python 2 compatible version of `unittest.TestCase.assertWarns`."""
def __init__(self, test_case, warning_type):
self.test_case = test_case
self.warning_type = warning_type
self.log = None
super(AssertWarns, self).__init__(record=True, module=None)


def __enter__(self):
self.log = super(AssertWarns, self).__enter__()
return self.log


def __exit__(self, *exc_info):
super(AssertWarns, self).__exit__(*exc_info)
self.test_case.assertEqual(type(self.log[0]), self.warning_type)

This can be called similarly to unittest.TestCase.assertWarns:

with AssertWarns(self, warnings.WarningMessage):
warnings.warn('test warning!')

where self is a unittest.TestCase.