Testings Python warnings

Wed 26 June 2013

If you're unittest-ing your code and that code issues some warnings using warnings.warn you should make some assertions for those messages too.

You probably do it because you're deprecating some API in your library or want to inform the library user that he's doing something very bad. You should have some assertions so you're sure those warnings were issued.

There's warnings.catch_warnings that you can use but it has a couple of issues:

  • Warnings already issues cannot be captured. They are stored in a magic __warningregistry__ module attribute. Which module depends on the stacklevel arguments and/or the caller. It's certainly not documented in the warnings module's testing section.
  • Warnings are wrapped in objects, so if you want to just check some strings you have to do some additional processing.

So here's a assert method that clears that nasty __warningregistry__ from every module. Note that the method expects the messages to be issued in the given order. It could be changed but I think this is good - if you can't predict the order from the test then you are probably testing too much code from a single test method.

There's also an assertNoWarnings method but it just raises the warning as an exception instead of checking the message list. You should always be able to easily pinpoint causes of failures in tests.

from contextlib import contextmanager

class WarnAssertionsMixin:
    @contextmanager
    def assertNoWarnings(self):
        try:
            warnings.simplefilter("error")
            yield
        finally:
            warnings.resetwarnings()

    @contextmanager
    def assertWarnings(self, messages):
        """
        Asserts that the given messages are issued in the given order.
        """
        if not messages:
            raise RuntimeError("Use assertNoWarnings instead!")

        with warnings.catch_warnings(record=True) as warning_list:
            warnings.simplefilter("always")
            for mod in sys.modules.values():
                if hasattr(mod, '__warningregistry__'):
                    mod.__warningregistry__.clear()
            yield
            warning_list = [w.message.args[0] for w in warning_list]
            self.assertEquals(
                messages,
                warning_list
            )

# to use it just include it in your testcase, eg:
class MyTestCase(unittest.TestCase, WarnAssertionsMixin):
    ...

If you don't like mucking with all the modules you could monkey patch the warnings.warn method using the mock library. It won't work if you do things like from warnings import warn but it's a bit shorter:

import mock

@contextmanager
def assertWarnings(self, messages):
    if not messages:
        raise RuntimeError("Use assertNoWarnings instead!")

    with mock.patch("warnings.warn") as mock_warnings:
        yield
        warning_list = [call[1][0] for call in mock_warnings.mock_calls]
        self.assertEquals(
            messages,
            warning_list
        )

This entry was tagged as python testing